@hasna/todos 0.15.34 → 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 (93) hide show
  1. package/dist/cli/cloud-router.d.ts +53 -19
  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 +3551 -836
  10. package/dist/cli/stage-a.d.ts.map +1 -1
  11. package/dist/contracts.js +147 -57
  12. package/dist/db/migrations.d.ts.map +1 -1
  13. package/dist/db/plan-comments.d.ts +14 -0
  14. package/dist/db/plan-comments.d.ts.map +1 -0
  15. package/dist/db/projects.d.ts +23 -0
  16. package/dist/db/projects.d.ts.map +1 -1
  17. package/dist/db/schema.d.ts.map +1 -1
  18. package/dist/db/task-crud.d.ts.map +1 -1
  19. package/dist/db/task-lifecycle.d.ts.map +1 -1
  20. package/dist/db/task-lists.d.ts.map +1 -1
  21. package/dist/db/task-relations.d.ts.map +1 -1
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1994 -346
  25. package/dist/json-contracts.d.ts.map +1 -1
  26. package/dist/lib/comment-cursor.d.ts +19 -0
  27. package/dist/lib/comment-cursor.d.ts.map +1 -1
  28. package/dist/lib/import-export-bridge.d.ts.map +1 -1
  29. package/dist/lib/onboarding-fixtures.d.ts.map +1 -1
  30. package/dist/mcp/index.js +2491 -507
  31. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
  32. package/dist/mcp.js +11 -4
  33. package/dist/project-registration/authority.d.ts.map +1 -1
  34. package/dist/project-registration/sqlite.d.ts.map +1 -1
  35. package/dist/project-registration.js +362 -79
  36. package/dist/registry.d.ts +1 -1
  37. package/dist/registry.d.ts.map +1 -1
  38. package/dist/registry.js +154 -57
  39. package/dist/release-provenance.json +5 -5
  40. package/dist/sdk/index.d.ts +1 -1
  41. package/dist/sdk/index.d.ts.map +1 -1
  42. package/dist/sdk/index.js +35 -0
  43. package/dist/sdk/v1.generated.d.ts +144 -0
  44. package/dist/sdk/v1.generated.d.ts.map +1 -1
  45. package/dist/server/cloud.d.ts +12 -2
  46. package/dist/server/cloud.d.ts.map +1 -1
  47. package/dist/server/index.js +5247 -8429
  48. package/dist/server/openapi.d.ts +2104 -1314
  49. package/dist/server/openapi.d.ts.map +1 -1
  50. package/dist/server/v1.d.ts +2 -1
  51. package/dist/server/v1.d.ts.map +1 -1
  52. package/dist/storage/config.d.ts +12 -30
  53. package/dist/storage/config.d.ts.map +1 -1
  54. package/dist/storage/index.d.ts +2 -2
  55. package/dist/storage/index.d.ts.map +1 -1
  56. package/dist/storage/interfaces.d.ts +13 -2
  57. package/dist/storage/interfaces.d.ts.map +1 -1
  58. package/dist/storage/local-sqlite.d.ts.map +1 -1
  59. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  60. package/dist/storage/timestamp-backfill.d.ts +62 -0
  61. package/dist/storage/timestamp-backfill.d.ts.map +1 -0
  62. package/dist/storage.d.ts +2 -2
  63. package/dist/storage.d.ts.map +1 -1
  64. package/dist/storage.js +327 -75
  65. package/dist/task-subtree-transfer/authority.d.ts +27 -0
  66. package/dist/task-subtree-transfer/authority.d.ts.map +1 -0
  67. package/dist/task-subtree-transfer/backend.d.ts +9 -0
  68. package/dist/task-subtree-transfer/backend.d.ts.map +1 -0
  69. package/dist/task-subtree-transfer/canonical.d.ts +14 -0
  70. package/dist/task-subtree-transfer/canonical.d.ts.map +1 -0
  71. package/dist/task-subtree-transfer/http.d.ts +16 -0
  72. package/dist/task-subtree-transfer/http.d.ts.map +1 -0
  73. package/dist/task-subtree-transfer/index.d.ts +6 -0
  74. package/dist/task-subtree-transfer/index.d.ts.map +1 -0
  75. package/dist/task-subtree-transfer/planner.d.ts +24 -0
  76. package/dist/task-subtree-transfer/planner.d.ts.map +1 -0
  77. package/dist/task-subtree-transfer/postgres.d.ts +19 -0
  78. package/dist/task-subtree-transfer/postgres.d.ts.map +1 -0
  79. package/dist/task-subtree-transfer/schema-sql.d.ts +5 -0
  80. package/dist/task-subtree-transfer/schema-sql.d.ts.map +1 -0
  81. package/dist/task-subtree-transfer/sqlite.d.ts +16 -0
  82. package/dist/task-subtree-transfer/sqlite.d.ts.map +1 -0
  83. package/dist/task-subtree-transfer/types.d.ts +148 -0
  84. package/dist/task-subtree-transfer/types.d.ts.map +1 -0
  85. package/dist/task-subtree-transfer.d.ts +2 -0
  86. package/dist/task-subtree-transfer.d.ts.map +1 -0
  87. package/dist/task-subtree-transfer.js +1869 -0
  88. package/dist/testing.d.ts +41 -7
  89. package/dist/testing.d.ts.map +1 -1
  90. package/dist/testing.js +44 -1
  91. package/dist/types/index.d.ts +29 -2
  92. package/dist/types/index.d.ts.map +1 -1
  93. package/package.json +11 -4
package/dist/index.js CHANGED
@@ -1884,6 +1884,10 @@ var init_migrations = __esm(() => {
1884
1884
  );
1885
1885
  INSERT OR IGNORE INTO _migrations (id) VALUES (70);
1886
1886
  COMMIT;
1887
+ `,
1888
+ `BEGIN;
1889
+ INSERT OR IGNORE INTO _migrations (id) VALUES (71);
1890
+ COMMIT;
1887
1891
  `
1888
1892
  ];
1889
1893
  });
@@ -2557,6 +2561,8 @@ function ensureSchema(db) {
2557
2561
  ensureColumn("projects", "task_list_id", "TEXT");
2558
2562
  ensureColumn("projects", "task_prefix", "TEXT");
2559
2563
  ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
2564
+ ensureColumn("projects", "parent_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
2565
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_parent_id ON projects(parent_id)");
2560
2566
  ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
2561
2567
  ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
2562
2568
  ensureColumn("tasks", "short_id", "TEXT");
@@ -2670,6 +2676,20 @@ function ensureSchema(db) {
2670
2676
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
2671
2677
  ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
2672
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)");
2673
2693
  ensureColumn("projects", "machine_id", "TEXT");
2674
2694
  ensureColumn("projects", "synced_at", "TEXT");
2675
2695
  ensureColumn("tasks", "machine_id", "TEXT");
@@ -3651,6 +3671,7 @@ var init_machines = __esm(() => {
3651
3671
  "task_lists",
3652
3672
  "plans",
3653
3673
  "task_comments",
3674
+ "plan_comments",
3654
3675
  "sessions",
3655
3676
  "task_history",
3656
3677
  "webhooks",
@@ -4154,20 +4175,16 @@ var init_sync_utils = __esm(() => {
4154
4175
  // src/storage/config.ts
4155
4176
  var exports_config = {};
4156
4177
  __export(exports_config, {
4157
- parseStorageMode: () => parseStorageMode,
4158
4178
  parseStorageBackend: () => parseStorageBackend,
4159
4179
  loadTodosStorageConfig: () => loadTodosStorageConfig,
4160
4180
  loadStorageConfig: () => loadStorageConfig,
4161
4181
  isTodosShadowEnabled: () => isTodosShadowEnabled,
4162
- isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
4163
4182
  isTodosPostgresBackend: () => isTodosPostgresBackend,
4164
4183
  getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
4165
- getTodosStorageMode: () => getTodosStorageMode,
4166
4184
  getTodosStorageEnvName: () => getTodosStorageEnvName,
4167
4185
  getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
4168
4186
  getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
4169
4187
  getTodosStorageBackend: () => getTodosStorageBackend,
4170
- getStorageMode: () => getStorageMode,
4171
4188
  getStorageDatabaseUrl: () => getStorageDatabaseUrl,
4172
4189
  getStorageDatabaseEnv: () => getStorageDatabaseEnv,
4173
4190
  getCanonicalTodosRdsConfig: () => getCanonicalTodosRdsConfig,
@@ -4177,7 +4194,6 @@ __export(exports_config, {
4177
4194
  TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
4178
4195
  TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
4179
4196
  STORAGE_TABLES: () => STORAGE_TABLES,
4180
- REMOVED_STORAGE_MODE_ENV_KEYS: () => REMOVED_STORAGE_MODE_ENV_KEYS,
4181
4197
  CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
4182
4198
  CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
4183
4199
  CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
@@ -4232,9 +4248,6 @@ function loadStorageConfig(env = process.env) {
4232
4248
  function isTodosPostgresBackend(config) {
4233
4249
  return config.mode === "postgres";
4234
4250
  }
4235
- function isTodosRemoteStorageEnabled(config) {
4236
- return isTodosPostgresBackend(config);
4237
- }
4238
4251
  function assertTodosRemoteStorageConfig(config) {
4239
4252
  if (!isTodosPostgresBackend(config))
4240
4253
  return;
@@ -4250,32 +4263,15 @@ function parseStorageBackend(value) {
4250
4263
  return "sqlite";
4251
4264
  if (normalized === "postgres" || normalized === "postgresql")
4252
4265
  return "postgres";
4253
- if (["local", "remote", "cloud", "hybrid", "self_hosted"].includes(normalized)) {
4254
- 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.`);
4255
- }
4256
4266
  throw new Error(`Storage backend must be sqlite or postgres`);
4257
4267
  }
4258
- function parseStorageMode(value) {
4259
- return parseStorageBackend(value);
4260
- }
4261
4268
  function getTodosStorageBackend(env = process.env) {
4262
- for (const key of REMOVED_STORAGE_MODE_ENV_KEYS) {
4263
- if (Object.hasOwn(env, key) && env[key] !== undefined) {
4264
- 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.`);
4265
- }
4266
- }
4267
4269
  if (!getTodosStorageDatabaseUrl(env))
4268
4270
  return "sqlite";
4269
4271
  if (isTodosShadowEnabled(env))
4270
4272
  return "sqlite";
4271
4273
  return "postgres";
4272
4274
  }
4273
- function getTodosStorageMode(env = process.env) {
4274
- return getTodosStorageBackend(env);
4275
- }
4276
- function getStorageMode(env = process.env) {
4277
- return getTodosStorageBackend(env);
4278
- }
4279
4275
  function isTodosShadowEnabled(env = process.env) {
4280
4276
  return parseBoolean(readStorageEnv(env, "shadow").value, false);
4281
4277
  }
@@ -4342,7 +4338,7 @@ function parsePositiveInteger(value, fallback) {
4342
4338
  }
4343
4339
  return parsed;
4344
4340
  }
4345
- 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";
4346
4342
  var init_config = __esm(() => {
4347
4343
  TODOS_STORAGE_TABLES = [
4348
4344
  "todos_sync_records",
@@ -4381,12 +4377,6 @@ var init_config = __esm(() => {
4381
4377
  syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
4382
4378
  syncDryRun: "TODOS_SYNC_DRY_RUN"
4383
4379
  };
4384
- REMOVED_STORAGE_MODE_ENV_KEYS = [
4385
- "HASNA_TODOS_STORAGE_MODE",
4386
- "HASNA_TODOS_MODE",
4387
- "TODOS_STORAGE_MODE",
4388
- "TODOS_MODE"
4389
- ];
4390
4380
  });
4391
4381
 
4392
4382
  // src/storage/shadow-outbox-schema.ts
@@ -5045,17 +5035,24 @@ function createProject(input, db) {
5045
5035
  const id = uuid();
5046
5036
  const timestamp2 = now();
5047
5037
  const derivedSlug = slugify(input.name);
5048
- const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : slugify(input.task_list_id);
5038
+ const taskListId = input.task_list_id === undefined ? derivedSlug : slugify(input.task_list_id);
5049
5039
  if (!derivedSlug || !taskListId)
5050
5040
  throw new Error("Project name and task-list slug must be non-empty");
5051
5041
  const slugConflict = d.query("SELECT id FROM projects WHERE task_list_id = ? LIMIT 1").get(taskListId);
5052
5042
  if (slugConflict || !claimCanonicalSlug("project", "global", taskListId, id, d)) {
5053
5043
  throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${taskListId}" already exists`);
5054
5044
  }
5045
+ const parentId = input.parent_id ?? null;
5046
+ if (parentId !== null) {
5047
+ const parent = getProject(parentId, d);
5048
+ if (!parent)
5049
+ throw new ProjectNotFoundError(parentId);
5050
+ assertNotProjectAncestor(id, parentId, d);
5051
+ }
5055
5052
  const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
5056
5053
  const machineId = currentStorageMachineId(d);
5057
- d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
5058
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp2, timestamp2, machineId]);
5054
+ d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, parent_id, created_at, updated_at, machine_id)
5055
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, parentId, timestamp2, timestamp2, machineId]);
5059
5056
  return getProject(id, d);
5060
5057
  })();
5061
5058
  }
@@ -5080,6 +5077,60 @@ function listProjects(db) {
5080
5077
  const d = db || getDatabase();
5081
5078
  return d.query("SELECT * FROM projects ORDER BY name").all();
5082
5079
  }
5080
+ function orderProjectsParentFirst(projects) {
5081
+ const projectId = (project) => {
5082
+ const id = project.id;
5083
+ return typeof id === "string" && id.length > 0 ? id : null;
5084
+ };
5085
+ const parentId = (project) => {
5086
+ const parent = project.parent_id;
5087
+ return parent == null ? null : String(parent);
5088
+ };
5089
+ const byId = new Set;
5090
+ for (const project of projects) {
5091
+ const id = projectId(project);
5092
+ if (id !== null)
5093
+ byId.add(id);
5094
+ }
5095
+ const ordered = [];
5096
+ const emitted = new Set;
5097
+ let remaining = [...projects];
5098
+ let progress = true;
5099
+ while (progress && remaining.length > 0) {
5100
+ progress = false;
5101
+ const deferred = [];
5102
+ for (const project of remaining) {
5103
+ const parent = parentId(project);
5104
+ if (parent === null || emitted.has(parent) || !byId.has(parent)) {
5105
+ ordered.push(project);
5106
+ const id = projectId(project);
5107
+ if (id !== null)
5108
+ emitted.add(id);
5109
+ progress = true;
5110
+ } else {
5111
+ deferred.push(project);
5112
+ }
5113
+ }
5114
+ remaining = deferred;
5115
+ }
5116
+ ordered.push(...remaining);
5117
+ return ordered;
5118
+ }
5119
+ function assertNotProjectAncestor(projectId, ancestorId, db) {
5120
+ const d = db || getDatabase();
5121
+ let cursor = ancestorId;
5122
+ const seen = new Set;
5123
+ while (cursor !== null) {
5124
+ if (cursor === projectId) {
5125
+ throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
5126
+ }
5127
+ if (seen.has(cursor))
5128
+ break;
5129
+ seen.add(cursor);
5130
+ const row = d.query("SELECT parent_id FROM projects WHERE id = ?").get(cursor);
5131
+ cursor = row?.parent_id ?? null;
5132
+ }
5133
+ }
5083
5134
  function updateProject(id, input, db) {
5084
5135
  const d = db || getDatabase();
5085
5136
  const project = getProject(id, d);
@@ -5102,6 +5153,16 @@ function updateProject(id, input, db) {
5102
5153
  sets.push("path = ?");
5103
5154
  params.push(input.path);
5104
5155
  }
5156
+ if (input.parent_id !== undefined) {
5157
+ if (input.parent_id !== null) {
5158
+ const parent = getProject(input.parent_id, d);
5159
+ if (!parent)
5160
+ throw new ProjectNotFoundError(input.parent_id);
5161
+ assertNotProjectAncestor(id, input.parent_id, d);
5162
+ }
5163
+ sets.push("parent_id = ?");
5164
+ params.push(input.parent_id);
5165
+ }
5105
5166
  params.push(id);
5106
5167
  d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
5107
5168
  return getProject(id, d);
@@ -6780,22 +6841,31 @@ function updateTaskList(id, input, db) {
6780
6841
  throw new TaskListNotFoundError(id);
6781
6842
  const sets = ["updated_at = ?"];
6782
6843
  const params = [now()];
6783
- if (input.slug !== undefined) {
6784
- const slug = slugify(input.slug);
6785
- if (!slug)
6786
- throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
6787
- 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);
6788
6852
  if (duplicate) {
6789
- 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`);
6790
6854
  }
6791
- if (slug !== existing.slug) {
6855
+ if (scopeChanged || effectiveSlug !== existing.slug) {
6792
6856
  releaseCanonicalSlugClaims("task_list", id, d);
6793
- if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(existing.project_id), slug, id, d)) {
6794
- 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`);
6795
6859
  }
6796
6860
  }
6861
+ }
6862
+ if (input.slug !== undefined) {
6797
6863
  sets.push("slug = ?");
6798
- params.push(slug);
6864
+ params.push(effectiveSlug);
6865
+ }
6866
+ if (projectId !== existing.project_id) {
6867
+ sets.push("project_id = ?");
6868
+ params.push(projectId);
6799
6869
  }
6800
6870
  if (input.name !== undefined) {
6801
6871
  sets.push("name = ?");
@@ -9244,8 +9314,8 @@ function failTask(id, agentId, reason, options, db) {
9244
9314
  const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
9245
9315
  const timestamp2 = now();
9246
9316
  const failTx = d.transaction(() => {
9247
- const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
9248
- 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]);
9249
9319
  if (res.changes === 0) {
9250
9320
  const current = getTask(id, d);
9251
9321
  throw new VersionConflictError(id, task.version, current?.version ?? -1);
@@ -9257,6 +9327,7 @@ function failTask(id, agentId, reason, options, db) {
9257
9327
  status: "failed",
9258
9328
  locked_by: null,
9259
9329
  locked_at: null,
9330
+ completed_at: task.completed_at ?? timestamp2,
9260
9331
  reason: safeReason,
9261
9332
  metadata: safeMeta,
9262
9333
  version: task.version + 1,
@@ -9905,13 +9976,20 @@ function updateTaskStored(id, input, db) {
9905
9976
  if (isTerminalStatus(input.status)) {
9906
9977
  sets.push("locked_by = NULL");
9907
9978
  sets.push("locked_at = NULL");
9908
- }
9909
- if (input.status === "completed") {
9910
- sets.push("completed_at = ?");
9911
- 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
+ }
9912
9986
  } else if (task.status === "completed" && input.completed_at === undefined) {
9913
9987
  sets.push("completed_at = NULL");
9914
9988
  }
9989
+ if (input.status === "in_progress") {
9990
+ sets.push("started_at = COALESCE(started_at, ?)");
9991
+ params.push(timestamp2);
9992
+ }
9915
9993
  }
9916
9994
  if (input.priority !== undefined) {
9917
9995
  sets.push("priority = ?");
@@ -10049,7 +10127,6 @@ function updateTaskStored(id, input, db) {
10049
10127
  if (input.approved_by !== undefined)
10050
10128
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
10051
10129
  const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
10052
- const completedNow = input.status === "completed";
10053
10130
  const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
10054
10131
  const updatedTask = {
10055
10132
  ...task,
@@ -10060,7 +10137,8 @@ function updateTaskStored(id, input, db) {
10060
10137
  updated_at: timestamp2,
10061
10138
  locked_by: terminalNow ? null : task.locked_by,
10062
10139
  locked_at: terminalNow ? null : task.locked_at,
10063
- 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,
10064
10142
  sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
10065
10143
  actual_minutes: input.actual_minutes ?? task.actual_minutes,
10066
10144
  confidence: input.confidence !== undefined ? input.confidence : task.confidence,
@@ -10121,6 +10199,9 @@ function deleteTask(id, db) {
10121
10199
  version: row.version
10122
10200
  }, d);
10123
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
+ }
10124
10205
  return result.changes > 0;
10125
10206
  }
10126
10207
  var init_task_crud = __esm(() => {
@@ -12805,10 +12886,10 @@ function cancelDispatch(id, db) {
12805
12886
  _db2.run("UPDATE dispatches SET status = 'cancelled' WHERE id = ?", [id]);
12806
12887
  return getDispatch(id, _db2);
12807
12888
  }
12808
- function updateDispatchStatus(id, status2, opts = {}, db) {
12889
+ function updateDispatchStatus(id, status3, opts = {}, db) {
12809
12890
  const _db2 = db ?? getDatabase();
12810
12891
  const error = opts.error ? sanitizePreWriteText(opts.error, "dispatch.error") : null;
12811
- _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]);
12812
12893
  }
12813
12894
  function createDispatchLog(log, db) {
12814
12895
  const _db2 = db ?? getDatabase();
@@ -12841,7 +12922,7 @@ var init_dispatches = __esm(() => {
12841
12922
  // package.json
12842
12923
  var package_default = {
12843
12924
  name: "@hasna/todos",
12844
- version: "0.15.34",
12925
+ version: "0.15.41",
12845
12926
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12846
12927
  type: "module",
12847
12928
  main: "dist/index.js",
@@ -12887,6 +12968,10 @@ var package_default = {
12887
12968
  "./task-manifest": {
12888
12969
  types: "./dist/task-manifest.d.ts",
12889
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"
12890
12975
  }
12891
12976
  },
12892
12977
  workspaces: [
@@ -12900,8 +12985,8 @@ var package_default = {
12900
12985
  "README.md"
12901
12986
  ],
12902
12987
  scripts: {
12903
- 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",
12904
- "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/*'",
12905
12990
  migrate: "bun run src/server/index.ts migrate",
12906
12991
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
12907
12992
  "generate:sdk": "bun run scripts/generate-sdk.ts",
@@ -12916,6 +13001,9 @@ var package_default = {
12916
13001
  "verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
12917
13002
  "verify:attested-container-candidate": "bun run scripts/attested-container-candidate.ts verify",
12918
13003
  "test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts",
13004
+ "emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
13005
+ "verify:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts verify",
13006
+ "test:attested-container-compatibility-vector": "bun test scripts/attested-container-compatibility-vector.test.ts",
12919
13007
  "issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
12920
13008
  prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
12921
13009
  postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
@@ -12952,7 +13040,7 @@ var package_default = {
12952
13040
  author: "Andrei Hasna <andrei@hasna.com>",
12953
13041
  license: "Apache-2.0",
12954
13042
  dependencies: {
12955
- "@hasna/contracts": "0.5.2",
13043
+ "@hasna/contracts": "0.13.3",
12956
13044
  "@hasna/events": "^0.1.11",
12957
13045
  "@modelcontextprotocol/sdk": "^1.12.1",
12958
13046
  chalk: "^5.4.1",
@@ -13604,6 +13692,7 @@ var TODOS_JSON_CONTRACTS = [
13604
13692
  task_list_id: nullableIdField,
13605
13693
  task_prefix: field(["string", "null"], "Optional task prefix.", true),
13606
13694
  task_counter: field("integer", "Monotonic project task counter."),
13695
+ parent_id: field(["string", "null"], "Optional parent project id; null means top-level.", true),
13607
13696
  created_at: isoDateField,
13608
13697
  updated_at: isoDateField
13609
13698
  },
@@ -16889,6 +16978,7 @@ function createAgentProjectDemoBundle() {
16889
16978
  task_list_id: ids.list,
16890
16979
  task_prefix: "DEMO",
16891
16980
  task_counter: 4,
16981
+ parent_id: null,
16892
16982
  created_at: createdAt,
16893
16983
  updated_at: completedAt
16894
16984
  });
@@ -25136,6 +25226,13 @@ var TODOS_PACKAGE_EXPORTS = [
25136
25226
  types: "./dist/task-manifest.d.ts",
25137
25227
  description: "Transactional package-owned authority for deterministic plan and task graph manifests.",
25138
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"
25139
25236
  }
25140
25237
  ];
25141
25238
  function source8(version) {
@@ -25328,6 +25425,40 @@ init_tasks();
25328
25425
  init_projects();
25329
25426
  init_plans();
25330
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
+
25331
25462
  // src/db/agents.ts
25332
25463
  init_types();
25333
25464
  init_database();
@@ -25985,6 +26116,20 @@ init_task_lists();
25985
26116
  init_templates();
25986
26117
  init_audit();
25987
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
25988
26133
  init_database();
25989
26134
 
25990
26135
  // src/db/integrity.ts
@@ -26529,6 +26674,7 @@ var PROJECT_COLUMNS = [
26529
26674
  "task_list_id",
26530
26675
  "task_prefix",
26531
26676
  "task_counter",
26677
+ "parent_id",
26532
26678
  "created_at",
26533
26679
  "updated_at",
26534
26680
  "machine_id",
@@ -26748,7 +26894,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
26748
26894
  }
26749
26895
  }
26750
26896
  };
26751
- applyRows("projects", "projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
26897
+ applyRows("projects", "projects", PROJECT_COLUMNS, orderProjectsParentFirst(snapshot.projects), "updated_at");
26752
26898
  applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
26753
26899
  applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
26754
26900
  applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
@@ -27080,7 +27226,22 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
27080
27226
  list: (projectId) => listPlans(projectId, database()),
27081
27227
  update: (id, input) => updatePlan(id, input, database()),
27082
27228
  completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
27083
- 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
+ }
27084
27245
  },
27085
27246
  planProjectLinks: {
27086
27247
  apply: (input) => applyPlanProjectLinkSqlite(input, database()),
@@ -27121,10 +27282,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
27121
27282
  throw new Error("Comment limit must be an integer between 1 and 1001");
27122
27283
  }
27123
27284
  let comments = listComments(taskId, database());
27124
- comments = comments.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
27285
+ comments = comments.sort(compareCommentKeyset);
27125
27286
  if (options2?.before) {
27126
27287
  const before = options2.before;
27127
- 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));
27128
27289
  }
27129
27290
  if (options2?.limit !== undefined)
27130
27291
  comments = comments.slice(-options2.limit);
@@ -27654,7 +27815,26 @@ function createPostgresTodosStorageAdapter(options) {
27654
27815
  list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
27655
27816
  update: (id, input) => updatePlan2(id, input, store),
27656
27817
  completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
27657
- 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
+ }
27658
27838
  },
27659
27839
  planProjectLinks: {
27660
27840
  apply: (input, context) => store.applyPlanProjectLink(input, context),
@@ -27872,6 +28052,27 @@ class PostgresJsonRecordStore {
27872
28052
  LIMIT $${params.length}`, params);
27873
28053
  return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
27874
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
+ }
27875
28076
  async listRecords(type) {
27876
28077
  await this.ensureSchema();
27877
28078
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
@@ -28203,8 +28404,12 @@ class PostgresJsonRecordStore {
28203
28404
  OR ($11::text <> $2
28204
28405
  AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
28205
28406
  AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
28206
- (SELECT count(*) FROM locked_plans) = jsonb_array_length($7::jsonb) AS all_plans_found,
28207
- ($8::text IS NULL OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
28407
+ (COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
28408
+ IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', '')) AS membership_changed,
28409
+ (NOT (COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
28410
+ IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', ''))
28411
+ OR $8::text IS NULL
28412
+ OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
28208
28413
  (SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
28209
28414
  ), guarded AS (
28210
28415
  SELECT
@@ -28227,7 +28432,6 @@ class PostgresJsonRecordStore {
28227
28432
  AND guarded.version_matches
28228
28433
  AND guarded.parent_found
28229
28434
  AND guarded.parent_acyclic
28230
- AND guarded.all_plans_found
28231
28435
  AND guarded.target_plan_found
28232
28436
  AND NOT guarded.project_conflict
28233
28437
  ON CONFLICT (service, object_type, object_id) DO UPDATE SET
@@ -28243,7 +28447,7 @@ class PostgresJsonRecordStore {
28243
28447
  RETURNING payload
28244
28448
  )
28245
28449
  SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
28246
- guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
28450
+ guarded.membership_changed, guarded.target_plan_found, guarded.project_conflict,
28247
28451
  (SELECT payload FROM stored) AS payload,
28248
28452
  (SELECT payload FROM locked_task) AS current_payload
28249
28453
  FROM guarded`, [
@@ -28275,7 +28479,7 @@ class PostgresJsonRecordStore {
28275
28479
  if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
28276
28480
  throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
28277
28481
  }
28278
- if (!row?.all_plans_found || !row.target_plan_found) {
28482
+ if (!row?.target_plan_found) {
28279
28483
  throw new PlanProjectLinkError("PLAN_PROJECT_LINK_PLAN_NOT_FOUND", `Plan membership changed through a missing plan: ${targetPlanId ?? planIds.join(", ")}`, { plan_ids: planIds, target_plan_id: targetPlanId });
28280
28484
  }
28281
28485
  if (row.project_conflict) {
@@ -29030,6 +29234,15 @@ class PostgresJsonRecordStore {
29030
29234
  WHERE r.service = $1 AND r.object_type = 'tasks' AND r.deleted_at IS NULL
29031
29235
  AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
29032
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
29033
29246
  ), deleted_plan AS (
29034
29247
  UPDATE ${this.tableName} r SET
29035
29248
  deleted_at = $3::timestamptz,
@@ -29205,7 +29418,8 @@ async function updateTask2(id, input, store, context) {
29205
29418
  task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
29206
29419
  parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
29207
29420
  created_by: existing.created_by,
29208
- 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
29209
29423
  };
29210
29424
  const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
29211
29425
  operation: "update",
@@ -29250,7 +29464,8 @@ async function failTask2(id, agentId, reason, options, store) {
29250
29464
  status: "failed",
29251
29465
  assigned_to: task2.assigned_to ?? agentId ?? null,
29252
29466
  reason: reason ?? task2.reason,
29253
- 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()
29254
29469
  }, store);
29255
29470
  if (!options?.retry)
29256
29471
  return { task: failed };
@@ -29487,17 +29702,26 @@ async function getChangedSince(since, filters, store) {
29487
29702
  async function createProject2(input, store, context) {
29488
29703
  const timestamp3 = new Date().toISOString();
29489
29704
  const derivedSlug = slugifyRaw(input.name);
29490
- const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : slugifyRaw(input.task_list_id);
29705
+ const taskListId = input.task_list_id === undefined ? derivedSlug : slugifyRaw(input.task_list_id);
29491
29706
  if (!derivedSlug || !taskListId)
29492
29707
  throw new Error("Project name and task-list slug must be non-empty");
29708
+ const parentId = input.parent_id ?? null;
29709
+ const id = randomUUID3();
29710
+ if (parentId !== null) {
29711
+ const parent = await store.get("projects", parentId);
29712
+ if (!parent)
29713
+ throw new ProjectNotFoundError(parentId);
29714
+ await assertNotProjectAncestorPostgres(id, parentId, store);
29715
+ }
29493
29716
  const project = {
29494
- id: randomUUID3(),
29717
+ id,
29495
29718
  name: input.name,
29496
29719
  path: input.path,
29497
29720
  description: input.description ?? null,
29498
29721
  task_list_id: taskListId,
29499
29722
  task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
29500
29723
  task_counter: 0,
29724
+ parent_id: parentId,
29501
29725
  created_at: timestamp3,
29502
29726
  updated_at: timestamp3,
29503
29727
  machine_id: store.machineId(context),
@@ -29505,12 +29729,38 @@ async function createProject2(input, store, context) {
29505
29729
  };
29506
29730
  return store.upsert("projects", project, context);
29507
29731
  }
29732
+ async function assertNotProjectAncestorPostgres(projectId, candidateParentId, store) {
29733
+ const all = await store.list("projects");
29734
+ const byId = new Map(all.map((project) => [project.id, project.parent_id ?? null]));
29735
+ let cursor = candidateParentId;
29736
+ const seen = new Set;
29737
+ while (cursor !== null) {
29738
+ if (cursor === projectId) {
29739
+ throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
29740
+ }
29741
+ if (seen.has(cursor))
29742
+ break;
29743
+ seen.add(cursor);
29744
+ cursor = byId.get(cursor) ?? null;
29745
+ }
29746
+ }
29508
29747
  async function updateProject2(id, input, store) {
29509
29748
  if ("task_list_id" in input) {
29510
29749
  throw new Error("task_list_id cannot be changed by updateProject; use renameProject for an atomic canonical rename");
29511
29750
  }
29512
29751
  const project = await requireRecord("projects", id, store);
29513
- const updated = { ...project, ...definedPatch(input), updated_at: new Date().toISOString() };
29752
+ if (input.parent_id !== undefined && input.parent_id !== null) {
29753
+ const parent = await store.get("projects", input.parent_id);
29754
+ if (!parent)
29755
+ throw new ProjectNotFoundError(input.parent_id);
29756
+ await assertNotProjectAncestorPostgres(id, input.parent_id, store);
29757
+ }
29758
+ const updated = {
29759
+ ...project,
29760
+ ...definedPatch(input),
29761
+ ...input.parent_id !== undefined ? { parent_id: input.parent_id } : {},
29762
+ updated_at: new Date().toISOString()
29763
+ };
29514
29764
  return store.upsert("projects", updated);
29515
29765
  }
29516
29766
  async function createPlan2(input, store, context) {
@@ -29663,16 +29913,22 @@ async function createTaskList2(input, store, context) {
29663
29913
  async function updateTaskList2(id, input, store) {
29664
29914
  const list = await requireRecord("task_lists", id, store);
29665
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;
29666
29917
  if (input.slug !== undefined) {
29667
29918
  const slug = slugifyRaw(input.slug);
29668
29919
  if (!slug)
29669
29920
  throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
29670
- 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);
29671
29926
  if (duplicate) {
29672
- 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`);
29673
29928
  }
29674
- patch.slug = slug;
29675
29929
  }
29930
+ if (projectId !== list.project_id)
29931
+ patch.project_id = projectId;
29676
29932
  return store.upsert("task_lists", {
29677
29933
  ...list,
29678
29934
  ...patch,
@@ -29765,6 +30021,22 @@ async function addComment2(input, store, context) {
29765
30021
  function redactComment(comment) {
29766
30022
  return { ...comment, content: redactEvidenceText(comment.content) };
29767
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
+ }
29768
30040
  async function exportSnapshot(store) {
29769
30041
  return {
29770
30042
  exportedAt: new Date().toISOString(),
@@ -35340,7 +35612,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
35340
35612
  }
35341
35613
  async createProject(input) {
35342
35614
  const derivedSlug = normalizeSlug(input.name);
35343
- const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : normalizeSlug(input.task_list_id);
35615
+ const taskListId = input.task_list_id === undefined ? derivedSlug : normalizeSlug(input.task_list_id);
35344
35616
  if (!derivedSlug || !taskListId) {
35345
35617
  throw new Error("Project name and task-list slug must be non-empty");
35346
35618
  }
@@ -35352,6 +35624,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
35352
35624
  task_list_id: taskListId,
35353
35625
  task_prefix: input.task_prefix ?? this.availableProjectPrefix(input.name),
35354
35626
  task_counter: 0,
35627
+ parent_id: input.parent_id ?? null,
35355
35628
  created_at: now(),
35356
35629
  updated_at: now(),
35357
35630
  machine_id: currentStorageMachineId(this.db)
@@ -35362,14 +35635,15 @@ class StagedSqliteTodosProjectRegistrationTransaction {
35362
35635
  try {
35363
35636
  const result = this.db.run(`INSERT INTO projects (
35364
35637
  id, name, path, description, task_list_id, task_prefix,
35365
- task_counter, created_at, updated_at, machine_id
35366
- ) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [
35638
+ task_counter, parent_id, created_at, updated_at, machine_id
35639
+ ) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [
35367
35640
  project.id,
35368
35641
  project.name,
35369
35642
  project.path,
35370
35643
  project.description,
35371
35644
  project.task_list_id,
35372
35645
  project.task_prefix,
35646
+ project.parent_id,
35373
35647
  project.created_at,
35374
35648
  project.updated_at,
35375
35649
  project.machine_id ?? null
@@ -35765,7 +36039,10 @@ function taskListSlug(projectSlug) {
35765
36039
  if (!slug || slug !== projectSlug) {
35766
36040
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "project_slug must be canonical kebab-case");
35767
36041
  }
35768
- return `todos-${slug}`;
36042
+ return slug;
36043
+ }
36044
+ function legacyTaskListSlug(projectSlug) {
36045
+ return `todos-${normalizeSlug(projectSlug)}`;
35769
36046
  }
35770
36047
  function deterministicTaskPrefix(projectSlug) {
35771
36048
  const letters = projectSlug.replace(/[^a-z0-9]/gi, "").toUpperCase();
@@ -36284,6 +36561,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
36284
36561
  created_by_operation: false
36285
36562
  };
36286
36563
  }
36564
+ if (request.bind_existing === true && conflict2.path === path && conflict2.task_list_id === legacyTaskListSlug(request.project_slug)) {
36565
+ return {
36566
+ record: boundExistingProjectRecord(conflict2),
36567
+ created_by_operation: false
36568
+ };
36569
+ }
36287
36570
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
36288
36571
  }
36289
36572
  await this.fault("before_object_write", request);
@@ -36320,6 +36603,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
36320
36603
  }
36321
36604
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
36322
36605
  }
36606
+ if (request.bind_existing === true) {
36607
+ const legacy = await transaction.findTaskListConflict(todosProjectId, legacyTaskListSlug(request.project_slug));
36608
+ if (legacy && legacy.project_id === todosProjectId) {
36609
+ return {
36610
+ record: boundExistingTaskListRecord(legacy),
36611
+ created_by_operation: false
36612
+ };
36613
+ }
36614
+ }
36323
36615
  await this.fault("before_object_write", request);
36324
36616
  const taskList = await transaction.createTaskList({
36325
36617
  name: request.project_name,
@@ -43483,6 +43775,1314 @@ class TodosTaskManifestHttpClient {
43483
43775
  function createTodosTaskManifestHttpClient(options) {
43484
43776
  return new TodosTaskManifestHttpClient(options);
43485
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
+ }
43486
45086
  // src/ai-tools.ts
43487
45087
  import { createHash as createHash16 } from "crypto";
43488
45088
  // src/cli/cloud-router.ts
@@ -43494,26 +45094,26 @@ var retryCapabilityCache = new Map;
43494
45094
  var taskCreatorCapabilityCache = new Map;
43495
45095
  var gitRefCapabilityCache = new Map;
43496
45096
  var remoteCommandCapabilityCache = new Map;
43497
- var LEGACY_STORAGE_MODE_KEYS = [
43498
- "HASNA_TODOS_STORAGE_MODE",
43499
- "HASNA_TODOS_MODE",
43500
- "TODOS_STORAGE_MODE",
43501
- "TODOS_MODE"
43502
- ];
43503
- function assertNoLegacyStorageMode(env = process.env) {
43504
- for (const key2 of LEGACY_STORAGE_MODE_KEYS) {
43505
- if (Object.hasOwn(env, key2) && env[key2] !== undefined) {
43506
- 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.`);
43507
- }
43508
- }
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));
43509
45111
  }
43510
- function resolveTodosCliStorageMode(env = process.env) {
43511
- assertNoLegacyStorageMode(env);
45112
+ function resolveTodosCliTransport(env = process.env) {
43512
45113
  const urlValue = env.HASNA_TODOS_API_URL?.trim();
43513
45114
  const keyValue = env.HASNA_TODOS_API_KEY?.trim();
43514
45115
  if (urlValue && keyValue) {
43515
45116
  return {
43516
- mode: "http",
43517
45117
  transport: "http",
43518
45118
  selected: true,
43519
45119
  source: "HASNA_TODOS_API_URL+HASNA_TODOS_API_KEY"
@@ -43525,15 +45125,15 @@ function resolveTodosCliStorageMode(env = process.env) {
43525
45125
  if (keyValue) {
43526
45126
  throw new Error("REMOTE_API_URL_MISSING: remote Todos storage requires HASNA_TODOS_API_URL; local SQLite fallback is disabled");
43527
45127
  }
45128
+ emitTodosLocalFallbackNotice(env);
43528
45129
  return {
43529
- mode: "sqlite",
43530
45130
  transport: "sqlite",
43531
45131
  selected: false,
43532
45132
  source: "default"
43533
45133
  };
43534
45134
  }
43535
45135
  function requestedTransport(env) {
43536
- return resolveTodosCliStorageMode(env).transport;
45136
+ return resolveTodosCliTransport(env).transport;
43537
45137
  }
43538
45138
  function normalizeRemoteAuthorityUrl(value) {
43539
45139
  const trimmed = value?.trim();
@@ -43567,13 +45167,13 @@ function normalizeRemoteAuthorityUrl(value) {
43567
45167
  function getTodosRemoteAuthorityConfigStatus(env = process.env) {
43568
45168
  let resolution;
43569
45169
  try {
43570
- resolution = resolveTodosCliStorageMode(env);
45170
+ resolution = resolveTodosCliTransport(env);
43571
45171
  } catch (error) {
43572
45172
  const issue = error instanceof Error ? error.message : String(error);
43573
45173
  return {
43574
45174
  selected: true,
43575
45175
  ok: false,
43576
- mode: "invalid",
45176
+ transport: "invalid",
43577
45177
  api_url_configured: Boolean(env.HASNA_TODOS_API_URL?.trim()),
43578
45178
  api_key_configured: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
43579
45179
  v1_base_url: null,
@@ -43581,12 +45181,12 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
43581
45181
  local_fallback: false
43582
45182
  };
43583
45183
  }
43584
- const { mode, selected } = resolution;
45184
+ const { transport, selected } = resolution;
43585
45185
  if (!selected) {
43586
45186
  return {
43587
45187
  selected: false,
43588
45188
  ok: true,
43589
- mode,
45189
+ transport,
43590
45190
  api_url_configured: false,
43591
45191
  api_key_configured: false,
43592
45192
  v1_base_url: null,
@@ -43611,7 +45211,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
43611
45211
  return {
43612
45212
  selected: true,
43613
45213
  ok: issues.length === 0,
43614
- mode,
45214
+ transport,
43615
45215
  api_url_configured: apiUrl !== null,
43616
45216
  api_key_configured: apiKeyConfigured,
43617
45217
  v1_base_url: apiUrl ? `${apiUrl}/v1` : null,
@@ -43620,24 +45220,24 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
43620
45220
  };
43621
45221
  }
43622
45222
  function requireTodosRemoteAuthorityEnv(env) {
43623
- const status2 = getTodosRemoteAuthorityConfigStatus(env);
43624
- if (!status2.ok)
43625
- throw new Error(status2.issues[0]);
45223
+ const status3 = getTodosRemoteAuthorityConfigStatus(env);
45224
+ if (!status3.ok)
45225
+ throw new Error(status3.issues[0]);
43626
45226
  return {
43627
45227
  ...env,
43628
- HASNA_TODOS_API_URL: status2.v1_base_url.replace(/\/v1$/, ""),
45228
+ HASNA_TODOS_API_URL: status3.v1_base_url.replace(/\/v1$/, ""),
43629
45229
  HASNA_TODOS_API_KEY: env.HASNA_TODOS_API_KEY.trim()
43630
45230
  };
43631
45231
  }
43632
45232
  function classifyRemoteRequestError(baseUrl, route, error) {
43633
- const status2 = error && typeof error === "object" ? error.status : undefined;
43634
- if (status2 === 401) {
45233
+ const status3 = error && typeof error === "object" ? error.status : undefined;
45234
+ if (status3 === 401) {
43635
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 });
43636
45236
  }
43637
- if (status2 === 403) {
45237
+ if (status3 === 403) {
43638
45238
  throw new Error(`REMOTE_API_FORBIDDEN: configured Todos authority ${baseUrl} denied ${route}; local SQLite fallback is disabled`, { cause: error });
43639
45239
  }
43640
- if (status2 === 409) {
45240
+ if (status3 === 409) {
43641
45241
  const body2 = error && typeof error === "object" ? error.body : undefined;
43642
45242
  if (body2 && typeof body2 === "object" && !Array.isArray(body2)) {
43643
45243
  const remoteError = body2;
@@ -43647,22 +45247,48 @@ function classifyRemoteRequestError(baseUrl, route, error) {
43647
45247
  }
43648
45248
  throw error;
43649
45249
  }
43650
- if (typeof status2 === "number" && status2 >= 300 && status2 < 400) {
45250
+ if (typeof status3 === "number" && status3 >= 300 && status3 < 400) {
43651
45251
  throw new Error(`REMOTE_API_REDIRECT_REJECTED: configured Todos authority ${baseUrl} redirected ${route}; ` + "authenticated redirects are disabled to prevent credential leakage", { cause: error });
43652
45252
  }
43653
- if (typeof status2 === "number" && status2 >= 500) {
43654
- 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 });
43655
45255
  }
43656
45256
  const message = error instanceof Error ? error.message : String(error);
43657
45257
  if (error instanceof Error && error.name === "AbortError" || /abort|timed?\s*out/i.test(message)) {
43658
- 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 });
43659
45260
  }
43660
- if (status2 === undefined) {
45261
+ if (status3 === undefined) {
43661
45262
  throw new Error(`REMOTE_API_UNREACHABLE: configured Todos authority ${baseUrl} could not be reached for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
43662
45263
  }
43663
45264
  throw error;
43664
45265
  }
43665
- function protectRemoteClient(client) {
45266
+ var REMOTE_REQUEST_TIMEOUT_MS = 1e4;
45267
+ function withBoundedRemoteRequest(options, requestTimeoutMs, run) {
45268
+ const controller = new AbortController;
45269
+ const onBaseAbort = () => controller.abort();
45270
+ if (options?.signal) {
45271
+ if (options.signal.aborted)
45272
+ controller.abort();
45273
+ else
45274
+ options.signal.addEventListener("abort", onBaseAbort, { once: true });
45275
+ }
45276
+ let timer;
45277
+ const deadline = new Promise((_, reject) => {
45278
+ timer = setTimeout(() => {
45279
+ controller.abort();
45280
+ reject(new DOMException(`Todos remote request exceeded the ${requestTimeoutMs}ms bounded request timeout`, "AbortError"));
45281
+ }, requestTimeoutMs);
45282
+ });
45283
+ const attempt = run({ ...options, signal: controller.signal });
45284
+ attempt.catch(() => {});
45285
+ return Promise.race([attempt, deadline]).finally(() => {
45286
+ if (timer)
45287
+ clearTimeout(timer);
45288
+ options?.signal?.removeEventListener("abort", onBaseAbort);
45289
+ });
45290
+ }
45291
+ function protectRemoteClient(client, requestTimeoutMs) {
43666
45292
  const baseUrl = remoteAuthorityBase(client);
43667
45293
  const protect = async (route, request) => {
43668
45294
  try {
@@ -43671,25 +45297,26 @@ function protectRemoteClient(client) {
43671
45297
  return classifyRemoteRequestError(baseUrl, route, error);
43672
45298
  }
43673
45299
  };
45300
+ const bounded = (options, run) => withBoundedRemoteRequest(options, requestTimeoutMs, run);
43674
45301
  const transport = client.transport;
43675
45302
  const protectedTransport = {
43676
45303
  baseUrl: transport.baseUrl,
43677
- request: (method, path, body2, options) => protect(path, () => transport.request(method, path, body2, options)),
43678
- get: (path, options) => protect(path, () => transport.get(path, options)),
43679
- post: (path, body2, options) => protect(path, () => transport.post(path, body2, options)),
43680
- put: (path, body2, options) => protect(path, () => transport.put(path, body2, options)),
43681
- patch: (path, body2, options) => protect(path, () => transport.patch(path, body2, options)),
43682
- del: (path, body2, options) => protect(path, () => transport.del(path, body2, options))
45304
+ request: (method, path, body2, options) => protect(path, () => bounded(options, (opts) => transport.request(method, path, body2, opts))),
45305
+ get: (path, options) => protect(path, () => bounded(options, (opts) => transport.get(path, opts))),
45306
+ post: (path, body2, options) => protect(path, () => bounded(options, (opts) => transport.post(path, body2, opts))),
45307
+ put: (path, body2, options) => protect(path, () => bounded(options, (opts) => transport.put(path, body2, opts))),
45308
+ patch: (path, body2, options) => protect(path, () => bounded(options, (opts) => transport.patch(path, body2, opts))),
45309
+ del: (path, body2, options) => protect(path, () => bounded(options, (opts) => transport.del(path, body2, opts)))
43683
45310
  };
43684
45311
  return {
43685
45312
  name: client.name,
43686
45313
  baseUrl: client.baseUrl,
43687
45314
  transport: protectedTransport,
43688
- list: (resource, options) => protect(`/${resource}`, () => client.list(resource, options)),
43689
- get: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.get(resource, id, options)),
43690
- create: (resource, body2, options) => protect(`/${resource}`, () => client.create(resource, body2, options)),
43691
- update: (resource, id, patch, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.update(resource, id, patch, options)),
43692
- delete: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.delete(resource, id, options))
45315
+ list: (resource, options) => protect(`/${resource}`, () => bounded(options, (opts) => client.list(resource, opts))),
45316
+ get: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.get(resource, id, opts))),
45317
+ create: (resource, body2, options) => protect(`/${resource}`, () => bounded(options, (opts) => client.create(resource, body2, opts))),
45318
+ update: (resource, id, patch, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.update(resource, id, patch, opts))),
45319
+ delete: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.delete(resource, id, opts)))
43693
45320
  };
43694
45321
  }
43695
45322
  function remoteAuthorityBase(client) {
@@ -43699,8 +45326,8 @@ async function requiredRemoteRoute(client, route, request, recognized404Codes =
43699
45326
  try {
43700
45327
  return await request();
43701
45328
  } catch (error) {
43702
- const status2 = error && typeof error === "object" ? error.status : undefined;
43703
- if (status2 === 404 || status2 === 405) {
45329
+ const status3 = error && typeof error === "object" ? error.status : undefined;
45330
+ if (status3 === 404 || status3 === 405) {
43704
45331
  const body2 = error && typeof error === "object" ? error.body : undefined;
43705
45332
  const code = body2 && typeof body2 === "object" && !Array.isArray(body2) ? body2.code : undefined;
43706
45333
  if (typeof code === "string" && recognized404Codes.includes(code))
@@ -43710,17 +45337,17 @@ async function requiredRemoteRoute(client, route, request, recognized404Codes =
43710
45337
  throw error;
43711
45338
  }
43712
45339
  }
43713
- function getTodosCloudClient(env = process.env) {
45340
+ function getTodosCloudClient(env = process.env, requestTimeoutMs = REMOTE_REQUEST_TIMEOUT_MS) {
43714
45341
  if (requestedTransport(env) !== "http")
43715
45342
  return null;
43716
45343
  const resolved = resolveStorageClient("todos", requireTodosRemoteAuthorityEnv(env), {
43717
- fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" })
45344
+ fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
45345
+ timeoutMs: requestTimeoutMs
43718
45346
  });
43719
- if (resolved.transport === "cloud-http")
43720
- return protectRemoteClient(resolved.client);
43721
45347
  const transportName = resolved.transport;
43722
- if (transportName === "http")
43723
- return protectRemoteClient(resolved.client);
45348
+ if (transportName === "cloud-http" || transportName === "http") {
45349
+ return protectRemoteClient(resolved.client, requestTimeoutMs);
45350
+ }
43724
45351
  return null;
43725
45352
  }
43726
45353
  function unwrapTask(raw) {
@@ -43871,9 +45498,9 @@ async function cloudListTasks(client, filter = {}) {
43871
45498
  const { status: _status, limit, offset, ...baseFilter } = filter;
43872
45499
  const start = offset ?? 0;
43873
45500
  const windowEnd = typeof limit === "number" ? start + limit : undefined;
43874
- const pages = await Promise.all(statuses.map((status2) => requestCloudTaskPage(client, {
45501
+ const pages = await Promise.all(statuses.map((status3) => requestCloudTaskPage(client, {
43875
45502
  ...baseFilter,
43876
- status: status2,
45503
+ status: status3,
43877
45504
  ...windowEnd === undefined ? {} : { limit: windowEnd }
43878
45505
  })));
43879
45506
  const seen = new Set;
@@ -44151,11 +45778,11 @@ init_audit();
44151
45778
  init_database();
44152
45779
  function upsertCheckpoint(task_id, step, updates, db) {
44153
45780
  const d = db || getDatabase();
44154
- const timestamp4 = now();
45781
+ const timestamp5 = now();
44155
45782
  const existing = d.query("SELECT id FROM task_checkpoints WHERE task_id = ? AND step = ?").get(task_id, step);
44156
45783
  const id = existing?.id ?? uuid();
44157
45784
  const agentId = updates.agent_id ?? null;
44158
- const status2 = updates.status ?? "pending";
45785
+ const status3 = updates.status ?? "pending";
44159
45786
  const data = updates.data ? JSON.stringify(updates.data) : JSON.stringify({});
44160
45787
  const error = updates.error ?? null;
44161
45788
  const attempt = updates.attempt ?? 1;
@@ -44169,22 +45796,22 @@ function upsertCheckpoint(task_id, step, updates, db) {
44169
45796
  task_id,
44170
45797
  agentId,
44171
45798
  step,
44172
- status2,
45799
+ status3,
44173
45800
  data,
44174
45801
  error,
44175
45802
  attempt,
44176
45803
  maxAttempts,
44177
45804
  startedAt,
44178
45805
  completedAt2,
44179
- timestamp4,
44180
- status2,
45806
+ timestamp5,
45807
+ status3,
44181
45808
  data,
44182
45809
  error,
44183
45810
  attempt,
44184
45811
  maxAttempts,
44185
45812
  startedAt,
44186
45813
  completedAt2,
44187
- timestamp4
45814
+ timestamp5
44188
45815
  ]);
44189
45816
  return rowToCheckpoint(d.query("SELECT * FROM task_checkpoints WHERE id = ?").get(id));
44190
45817
  }
@@ -44247,10 +45874,10 @@ function stepForGate(gate) {
44247
45874
  throw new Error("Approval gate name is required");
44248
45875
  return `approval:${trimmed}`;
44249
45876
  }
44250
- function approvalStatusToCheckpointStatus(status2) {
44251
- if (status2 === "approved")
45877
+ function approvalStatusToCheckpointStatus(status3) {
45878
+ if (status3 === "approved")
44252
45879
  return "completed";
44253
- if (status2 === "pending")
45880
+ if (status3 === "pending")
44254
45881
  return "pending";
44255
45882
  return "failed";
44256
45883
  }
@@ -44272,12 +45899,12 @@ function isExpired(expiresAt, at = new Date) {
44272
45899
  return Boolean(expiresAt && new Date(expiresAt).getTime() <= at.getTime());
44273
45900
  }
44274
45901
  function gateFromCheckpoint(checkpoint) {
44275
- const status2 = checkpointStatusToApprovalStatus(checkpoint);
45902
+ const status3 = checkpointStatusToApprovalStatus(checkpoint);
44276
45903
  return {
44277
45904
  id: checkpoint.id,
44278
45905
  task_id: checkpoint.task_id,
44279
45906
  gate: String(checkpoint.data["approval_gate_name"] || checkpoint.step.replace(/^approval:/, "")),
44280
- status: status2,
45907
+ status: status3,
44281
45908
  reviewer: typeof checkpoint.data["reviewer"] === "string" ? checkpoint.data["reviewer"] : null,
44282
45909
  requester: typeof checkpoint.data["requester"] === "string" ? checkpoint.data["requester"] : null,
44283
45910
  reason: typeof checkpoint.data["reason"] === "string" ? checkpoint.data["reason"] : null,
@@ -44322,13 +45949,13 @@ function logApprovalEvent(taskId, action, gate, agentId, db) {
44322
45949
  });
44323
45950
  }
44324
45951
  }
44325
- function writeGate(input, status2, decision, db) {
45952
+ function writeGate(input, status3, decision, db) {
44326
45953
  const d = db || getDatabase();
44327
45954
  ensureTask(input.task_id, d);
44328
45955
  const step = stepForGate(input.gate);
44329
45956
  const existing = getCheckpoint(input.task_id, step, d);
44330
45957
  const existingData = existing?.data || {};
44331
- const timestamp4 = decision?.decided_at || now();
45958
+ const timestamp5 = decision?.decided_at || now();
44332
45959
  const existingRunId = typeof existingData["run_id"] === "string" ? existingData["run_id"] : undefined;
44333
45960
  const runId = input.run_id ? resolveTaskRunId(input.run_id, d) : existingRunId;
44334
45961
  const data = {
@@ -44336,7 +45963,7 @@ function writeGate(input, status2, decision, db) {
44336
45963
  ...input.metadata || {},
44337
45964
  approval_gate: true,
44338
45965
  approval_gate_name: input.gate.trim(),
44339
- approval_status: status2,
45966
+ approval_status: status3,
44340
45967
  requester: input.requester ?? existingData["requester"] ?? null,
44341
45968
  reviewer: input.reviewer ?? decision?.decided_by ?? existingData["reviewer"] ?? null,
44342
45969
  reason: decision?.reason ?? input.reason ?? existingData["reason"] ?? null,
@@ -44349,11 +45976,11 @@ function writeGate(input, status2, decision, db) {
44349
45976
  };
44350
45977
  const checkpoint = upsertCheckpoint(input.task_id, step, {
44351
45978
  agent_id: decision?.decided_by || input.requester || input.reviewer,
44352
- status: approvalStatusToCheckpointStatus(status2),
45979
+ status: approvalStatusToCheckpointStatus(status3),
44353
45980
  data,
44354
- error: status2 === "rejected" || status2 === "expired" ? String(data.reason || status2) : null,
44355
- started_at: existing?.started_at || timestamp4,
44356
- 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
44357
45984
  }, d);
44358
45985
  return gateFromCheckpoint(checkpoint);
44359
45986
  }
@@ -45201,7 +46828,7 @@ async function executeListTasks(adapter, state, request, input) {
45201
46828
  const planId = boundedOptionalString(record, "plan_id", TODOS_AI_READ_TOOL_LIMITS.max_identifier_bytes);
45202
46829
  const taskListId = boundedOptionalString(record, "task_list_id", TODOS_AI_READ_TOOL_LIMITS.max_identifier_bytes);
45203
46830
  const assignedTo = boundedOptionalString(record, "assigned_to", TODOS_AI_READ_TOOL_LIMITS.max_filter_string_bytes);
45204
- const status2 = boundedEnum(record, "status", TASK_STATUSES);
46831
+ const status3 = boundedEnum(record, "status", TASK_STATUSES);
45205
46832
  const priority = boundedEnum(record, "priority", TASK_PRIORITIES);
45206
46833
  const tags = boundedTags(record);
45207
46834
  if (projectId !== undefined)
@@ -45212,8 +46839,8 @@ async function executeListTasks(adapter, state, request, input) {
45212
46839
  filter.task_list_id = taskListId;
45213
46840
  if (assignedTo !== undefined)
45214
46841
  filter.assigned_to = assignedTo;
45215
- if (status2 !== undefined)
45216
- filter.status = status2;
46842
+ if (status3 !== undefined)
46843
+ filter.status = status3;
45217
46844
  if (priority !== undefined)
45218
46845
  filter.priority = priority;
45219
46846
  if (tags !== undefined)
@@ -45555,29 +47182,29 @@ function getNativeStorageStatus(env = process.env) {
45555
47182
  };
45556
47183
  }
45557
47184
  function getNativeStorageSyncPlan(env = process.env, options = {}) {
45558
- const status2 = getNativeStorageStatus(env);
47185
+ const status3 = getNativeStorageStatus(env);
45559
47186
  const steps = [
45560
47187
  "Read local SQLite snapshot",
45561
- status2.remote_enabled ? "Prepare Postgres sync table upserts" : "Skip remote database writes in local mode",
45562
- 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",
45563
47190
  "Report planned changes without opening network connections"
45564
47191
  ];
45565
47192
  return {
45566
- ok: status2.ok,
47193
+ ok: status3.ok,
45567
47194
  service: "todos",
45568
47195
  dry_run: true,
45569
47196
  no_network: true,
45570
- status: status2,
47197
+ status: status3,
45571
47198
  postgres: {
45572
- required: status2.remote_enabled,
45573
- configured: status2.database.configured,
47199
+ required: status3.remote_enabled,
47200
+ configured: status3.database.configured,
45574
47201
  schema_sql: options.includeSchemaSql ? postgresTodosSyncSchemaSql() : []
45575
47202
  },
45576
47203
  object_storage: {
45577
- required: status2.remote_enabled,
45578
- configured: status2.object_storage.configured,
45579
- bucket: status2.object_storage.bucket,
45580
- 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
45581
47208
  },
45582
47209
  steps
45583
47210
  };
@@ -45683,8 +47310,8 @@ function resolveTaskList(task3, project, db) {
45683
47310
  }
45684
47311
  return null;
45685
47312
  }
45686
- function isTerminal2(status2) {
45687
- return status2 === "completed" || status2 === "cancelled" || status2 === "failed";
47313
+ function isTerminal2(status3) {
47314
+ return status3 === "completed" || status3 === "cancelled" || status3 === "failed";
45688
47315
  }
45689
47316
  function routeConcurrencyKey(task3, project, taskList, projectPath) {
45690
47317
  if (project?.id)
@@ -45865,7 +47492,7 @@ function setTaskWorkflowPointers(taskId, input, db) {
45865
47492
  latest_evaluation_path: pointerPatch(previous.latest_evaluation_path, input, "latest_evaluation_path"),
45866
47493
  workflow_state: pointerPatch(previous.workflow_state, input, "workflow_state")
45867
47494
  });
45868
- const timestamp4 = now();
47495
+ const timestamp5 = now();
45869
47496
  const {
45870
47497
  current_workflow_invocation_id,
45871
47498
  current_run_id,
@@ -45881,7 +47508,7 @@ function setTaskWorkflowPointers(taskId, input, db) {
45881
47508
  workflow_invocation: {
45882
47509
  schema_version: TASK_WORKFLOW_POINTER_SCHEMA_VERSION,
45883
47510
  ...next,
45884
- updated_at: timestamp4,
47511
+ updated_at: timestamp5,
45885
47512
  updated_by: input.actor ?? null
45886
47513
  }
45887
47514
  };
@@ -45915,8 +47542,8 @@ function normalizePath3(input) {
45915
47542
  return resolve10(input);
45916
47543
  }
45917
47544
  function sourceStoreId(sourceDbPath) {
45918
- const digest2 = createHash17("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
45919
- return `sqlite:${digest2}`;
47545
+ const digest3 = createHash17("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
47546
+ return `sqlite:${digest3}`;
45920
47547
  }
45921
47548
  function inferSourceRepoPath(sourceDbPath) {
45922
47549
  const normalized = normalizePath3(sourceDbPath);
@@ -46687,7 +48314,7 @@ function rowToSnapshot(row) {
46687
48314
  function saveSnapshot(input, db) {
46688
48315
  const d = db || getDatabase();
46689
48316
  const id = uuid();
46690
- const timestamp4 = now();
48317
+ const timestamp5 = now();
46691
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)
46692
48319
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
46693
48320
  id,
@@ -46701,7 +48328,7 @@ function saveSnapshot(input, db) {
46701
48328
  JSON.stringify(input.blockers || []),
46702
48329
  input.next_steps || null,
46703
48330
  JSON.stringify(input.metadata || {}),
46704
- timestamp4
48331
+ timestamp5
46705
48332
  ]);
46706
48333
  return {
46707
48334
  id,
@@ -46715,7 +48342,7 @@ function saveSnapshot(input, db) {
46715
48342
  blockers: input.blockers || [],
46716
48343
  next_steps: input.next_steps || null,
46717
48344
  metadata: input.metadata || {},
46718
- created_at: timestamp4
48345
+ created_at: timestamp5
46719
48346
  };
46720
48347
  }
46721
48348
  function getLatestSnapshot(agentId, taskId, db) {
@@ -46831,7 +48458,7 @@ function createKnowledgeRecord(input, db) {
46831
48458
  if (!title)
46832
48459
  throw new Error("Knowledge record title is required");
46833
48460
  const d = db || getDatabase();
46834
- const timestamp4 = now();
48461
+ const timestamp5 = now();
46835
48462
  const id = uuid();
46836
48463
  d.run(`INSERT INTO project_knowledge_records (
46837
48464
  id, record_type, title, content, decision, rationale, alternatives,
@@ -46851,8 +48478,8 @@ function createKnowledgeRecord(input, db) {
46851
48478
  input.snapshot_id || null,
46852
48479
  JSON.stringify(normalizeTags(input.tags)),
46853
48480
  JSON.stringify(input.metadata || {}),
46854
- timestamp4,
46855
- timestamp4
48481
+ timestamp5,
48482
+ timestamp5
46856
48483
  ]);
46857
48484
  return getKnowledgeRecord(id, d);
46858
48485
  }
@@ -47043,9 +48670,9 @@ function normalizeText4(value) {
47043
48670
  const trimmed = value.trim();
47044
48671
  return trimmed.length > 0 ? trimmed : null;
47045
48672
  }
47046
- function assertStatus(status2) {
47047
- if (!VALID_STATUSES.has(status2))
47048
- 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}`);
47049
48676
  }
47050
48677
  function assertSeverity(severity) {
47051
48678
  if (!VALID_SEVERITIES2.has(severity))
@@ -47090,16 +48717,16 @@ function createRisk(input, db) {
47090
48717
  const title = input.title.trim();
47091
48718
  if (!title)
47092
48719
  throw new Error("Risk title is required");
47093
- const status2 = input.status || "open";
48720
+ const status3 = input.status || "open";
47094
48721
  const severity = input.severity || "medium";
47095
48722
  const probability = input.probability || "medium";
47096
- assertStatus(status2);
48723
+ assertStatus(status3);
47097
48724
  assertSeverity(severity);
47098
48725
  assertProbability(probability);
47099
48726
  const d = db || getDatabase();
47100
- const timestamp4 = now();
48727
+ const timestamp5 = now();
47101
48728
  const id = uuid();
47102
- const closedAt = status2 === "resolved" || status2 === "accepted" ? timestamp4 : null;
48729
+ const closedAt = status3 === "resolved" || status3 === "accepted" ? timestamp5 : null;
47103
48730
  d.run(`INSERT INTO project_risks (
47104
48731
  id, title, description, status, severity, probability, owner, mitigation, due_at,
47105
48732
  project_id, plan_id, task_id, tags, metadata, created_at, updated_at, closed_at
@@ -47107,7 +48734,7 @@ function createRisk(input, db) {
47107
48734
  id,
47108
48735
  title,
47109
48736
  normalizeText4(input.description),
47110
- status2,
48737
+ status3,
47111
48738
  severity,
47112
48739
  probability,
47113
48740
  normalizeText4(input.owner),
@@ -47118,8 +48745,8 @@ function createRisk(input, db) {
47118
48745
  resolveKnownId2("tasks", input.task_id, d),
47119
48746
  JSON.stringify(normalizeTags2(input.tags)),
47120
48747
  JSON.stringify(input.metadata || {}),
47121
- timestamp4,
47122
- timestamp4,
48748
+ timestamp5,
48749
+ timestamp5,
47123
48750
  closedAt
47124
48751
  ]);
47125
48752
  return getRisk(id, d);
@@ -47202,8 +48829,8 @@ function updateRisk(id, input, db) {
47202
48829
  d.run(`UPDATE project_risks SET ${sets.join(", ")} WHERE id = ?`, params);
47203
48830
  return getRisk(resolved, d);
47204
48831
  }
47205
- function closeRisk(id, status2 = "resolved", db) {
47206
- return updateRisk(id, { status: status2 }, db);
48832
+ function closeRisk(id, status3 = "resolved", db) {
48833
+ return updateRisk(id, { status: status3 }, db);
47207
48834
  }
47208
48835
  function buildRiskFilters(options, db) {
47209
48836
  const conditions = [];
@@ -47331,7 +48958,7 @@ function scoreHealth(scope, scopeId, db) {
47331
48958
  };
47332
48959
  const totalPenalty = Object.values(penalties).reduce((sum, penalty) => sum + penalty, 0);
47333
48960
  const score = Math.max(0, 100 - Math.min(100, totalPenalty));
47334
- 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";
47335
48962
  const recommendations = [];
47336
48963
  if (blocked.length > 0)
47337
48964
  recommendations.push("Clear blocking dependencies before starting more work.");
@@ -47355,7 +48982,7 @@ function scoreHealth(scope, scopeId, db) {
47355
48982
  scope,
47356
48983
  scope_id: scopeInfo.id,
47357
48984
  score,
47358
- status: status2,
48985
+ status: status3,
47359
48986
  components: {
47360
48987
  total_tasks: tasks.length,
47361
48988
  blocked_tasks: blocked.length,
@@ -47602,7 +49229,7 @@ function createRetrospective(input, db) {
47602
49229
  const d = db || getDatabase();
47603
49230
  const report = buildReport(input, d);
47604
49231
  const scope = scopeFromInput(input, d);
47605
- const timestamp4 = now();
49232
+ const timestamp5 = now();
47606
49233
  if (input.create_followups) {
47607
49234
  for (const followUp of report.follow_up_tasks) {
47608
49235
  const task3 = createTask({
@@ -47618,7 +49245,7 @@ function createRetrospective(input, db) {
47618
49245
  }
47619
49246
  const id = uuid();
47620
49247
  d.run(`INSERT INTO local_retrospectives (id, title, scope, project_id, plan_id, agent_id, report_json, created_at, updated_at)
47621
- 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]);
47622
49249
  return getRetrospective(id, d);
47623
49250
  }
47624
49251
  function getRetrospective(id, db) {
@@ -47793,7 +49420,7 @@ function addSourceOnce(projectId, type, name, uri, metadata, db) {
47793
49420
  function bootstrapProject(options = {}, db) {
47794
49421
  const d = db || getDatabase();
47795
49422
  const discovery = discoverProjectWorkspace(options.path);
47796
- const taskListSlug2 = options.taskListSlug || `todos-${slugify(options.name || discovery.projectName)}`;
49423
+ const taskListSlug2 = options.taskListSlug || slugify(options.name || discovery.projectName);
47797
49424
  if (options.dryRun) {
47798
49425
  return {
47799
49426
  dryRun: true,
@@ -47907,7 +49534,7 @@ function createApiKey(input, db) {
47907
49534
  if (!name)
47908
49535
  throw new Error("API key name is required");
47909
49536
  const key2 = generatePlaintextKey();
47910
- const timestamp4 = now();
49537
+ const timestamp5 = now();
47911
49538
  const id = uuid();
47912
49539
  const prefix = key2.slice(0, 12);
47913
49540
  d.run(`INSERT INTO api_keys (id, name, key_hash, prefix, permissions, created_at, expires_at)
@@ -47917,7 +49544,7 @@ function createApiKey(input, db) {
47917
49544
  hashApiKey(key2),
47918
49545
  prefix,
47919
49546
  JSON.stringify(input.permissions?.length ? input.permissions : ["*"]),
47920
- timestamp4,
49547
+ timestamp5,
47921
49548
  input.expires_at || null
47922
49549
  ]);
47923
49550
  const row = d.query("SELECT * FROM api_keys WHERE id = ?").get(id);
@@ -47971,15 +49598,15 @@ function rowToSession(row) {
47971
49598
  function createSession(input, db) {
47972
49599
  const d = db || getDatabase();
47973
49600
  const id = uuid();
47974
- const timestamp4 = now();
49601
+ const timestamp5 = now();
47975
49602
  d.run(`INSERT INTO sessions (id, agent_id, project_id, working_dir, started_at, last_activity, metadata)
47976
49603
  VALUES (?, ?, ?, ?, ?, ?, ?)`, [
47977
49604
  id,
47978
49605
  input.agent_id || null,
47979
49606
  input.project_id || null,
47980
49607
  input.working_dir || null,
47981
- timestamp4,
47982
- timestamp4,
49608
+ timestamp5,
49609
+ timestamp5,
47983
49610
  JSON.stringify(input.metadata || {})
47984
49611
  ]);
47985
49612
  return getSession(id, d);
@@ -48152,7 +49779,7 @@ function rowToArtifact2(row) {
48152
49779
  function addArtifact(input, db, dbPath) {
48153
49780
  const d = db || getDatabase();
48154
49781
  const id = uuid();
48155
- const timestamp4 = now();
49782
+ const timestamp5 = now();
48156
49783
  const storageMode = input.storage_mode ?? "copy";
48157
49784
  const stored = storeArtifactFile({
48158
49785
  artifactId: id,
@@ -48177,8 +49804,8 @@ function addArtifact(input, db, dbPath) {
48177
49804
  stored.sizeBytes,
48178
49805
  input.redaction_status ?? "none",
48179
49806
  JSON.stringify(input.metadata ?? {}),
48180
- timestamp4,
48181
- timestamp4
49807
+ timestamp5,
49808
+ timestamp5
48182
49809
  ]);
48183
49810
  return getArtifact(id, d);
48184
49811
  }
@@ -48211,14 +49838,14 @@ function listArtifacts(filter = {}, db) {
48211
49838
  }
48212
49839
  function updateArtifactRedaction(id, redactionStatus, db) {
48213
49840
  const d = db || getDatabase();
48214
- const timestamp4 = now();
48215
- 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]);
48216
49843
  return getArtifact(id, d);
48217
49844
  }
48218
49845
  function softDeleteArtifact(id, db) {
48219
49846
  const d = db || getDatabase();
48220
- const timestamp4 = now();
48221
- 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]);
48222
49849
  return result.changes > 0;
48223
49850
  }
48224
49851
  function purgeArtifact(id, db, dbPath) {
@@ -48356,7 +49983,7 @@ init_redaction();
48356
49983
  function createHandoff(input, db) {
48357
49984
  const d = db || getDatabase();
48358
49985
  const id = uuid();
48359
- const timestamp4 = now();
49986
+ const timestamp5 = now();
48360
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)
48361
49988
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
48362
49989
  id,
@@ -48371,7 +49998,7 @@ function createHandoff(input, db) {
48371
49998
  toJson(input.task_ids),
48372
49999
  toJson(input.relevant_files),
48373
50000
  toJson(input.run_ids),
48374
- timestamp4
50001
+ timestamp5
48375
50002
  ]);
48376
50003
  return {
48377
50004
  id,
@@ -48386,7 +50013,7 @@ function createHandoff(input, db) {
48386
50013
  task_ids: input.task_ids || null,
48387
50014
  relevant_files: input.relevant_files || null,
48388
50015
  run_ids: input.run_ids || null,
48389
- created_at: timestamp4,
50016
+ created_at: timestamp5,
48390
50017
  acknowledged_by: []
48391
50018
  };
48392
50019
  }
@@ -48898,7 +50525,7 @@ function upsertVerificationProvider(input) {
48898
50525
  const name = normalizeName(input.name);
48899
50526
  const config = loadConfig();
48900
50527
  const existing = config.verification_providers?.[name];
48901
- const timestamp4 = new Date().toISOString();
50528
+ const timestamp5 = new Date().toISOString();
48902
50529
  const provider = {
48903
50530
  ...existing,
48904
50531
  name,
@@ -48909,8 +50536,8 @@ function upsertVerificationProvider(input) {
48909
50536
  capabilities: input.capabilities ?? existing?.capabilities,
48910
50537
  retry: input.retry ? retryConfig(input.retry) : existing?.retry,
48911
50538
  timeout_ms: timeoutMs(input.timeout_ms ?? existing?.timeout_ms),
48912
- created_at: existing?.created_at || timestamp4,
48913
- updated_at: timestamp4
50539
+ created_at: existing?.created_at || timestamp5,
50540
+ updated_at: timestamp5
48914
50541
  };
48915
50542
  saveConfig({
48916
50543
  ...config,
@@ -49471,7 +51098,7 @@ function upsertPolicyPack(input) {
49471
51098
  const existing = config.policy_packs?.[input.name];
49472
51099
  const root = normalizePath4(input.root || existing?.root || process.cwd());
49473
51100
  const base = existing || defaultPolicyPack(input.name, root);
49474
- const timestamp4 = new Date().toISOString();
51101
+ const timestamp5 = new Date().toISOString();
49475
51102
  const pack = {
49476
51103
  ...base,
49477
51104
  name: input.name,
@@ -49489,8 +51116,8 @@ function upsertPolicyPack(input) {
49489
51116
  require_artifact: input.require_artifact ?? base.require_artifact,
49490
51117
  evidence_min_count: input.evidence_min_count ?? base.evidence_min_count,
49491
51118
  branch_pattern: input.branch_pattern ?? base.branch_pattern,
49492
- created_at: existing?.created_at || timestamp4,
49493
- updated_at: timestamp4
51119
+ created_at: existing?.created_at || timestamp5,
51120
+ updated_at: timestamp5
49494
51121
  };
49495
51122
  saveConfig({
49496
51123
  ...config,
@@ -49832,7 +51459,7 @@ function summarizeOutput(stdout, stderr) {
49832
51459
  function upsertAgentRunAdapter(input) {
49833
51460
  const config = loadConfig();
49834
51461
  const existing = config.agent_run_adapters?.[input.name];
49835
- const timestamp4 = new Date().toISOString();
51462
+ const timestamp5 = new Date().toISOString();
49836
51463
  const adapter = {
49837
51464
  ...existing,
49838
51465
  name: input.name,
@@ -49840,8 +51467,8 @@ function upsertAgentRunAdapter(input) {
49840
51467
  sandbox: input.sandbox ?? existing?.sandbox,
49841
51468
  cwd: input.cwd ?? existing?.cwd,
49842
51469
  env: input.env ?? existing?.env,
49843
- created_at: existing?.created_at || timestamp4,
49844
- updated_at: timestamp4
51470
+ created_at: existing?.created_at || timestamp5,
51471
+ updated_at: timestamp5
49845
51472
  };
49846
51473
  saveConfig({
49847
51474
  ...config,
@@ -49965,7 +51592,7 @@ async function runNextAgentDispatch(input = {}, db) {
49965
51592
  proc.exited
49966
51593
  ]);
49967
51594
  const outputSummary = summarizeOutput(stdout, stderr);
49968
- const status2 = exitCode === 0 ? "completed" : "failed";
51595
+ const status3 = exitCode === 0 ? "completed" : "failed";
49969
51596
  addTaskRunCommand({
49970
51597
  run_id: next.run.id,
49971
51598
  command,
@@ -49974,15 +51601,15 @@ async function runNextAgentDispatch(input = {}, db) {
49974
51601
  output_summary: outputSummary ?? undefined,
49975
51602
  agent_id: next.run.agent_id ?? undefined
49976
51603
  }, d);
49977
- 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}` };
49978
51605
  updateDispatcherMetadata(next.run.id, completed, d);
49979
51606
  finishTaskRun({
49980
51607
  run_id: next.run.id,
49981
- status: status2,
49982
- summary: outputSummary || `agent run ${status2}`,
51608
+ status: status3,
51609
+ summary: outputSummary || `agent run ${status3}`,
49983
51610
  agent_id: next.run.agent_id ?? undefined
49984
51611
  }, d);
49985
- 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 };
49986
51613
  }
49987
51614
  function listAgentRuns(filter = {}, db) {
49988
51615
  let runs = listAgentRunQueue(db).map(toAgentRun);
@@ -50951,10 +52578,10 @@ function extractEmbeddedBridge(markdown) {
50951
52578
  const match = markdown.match(/<!--\s*hasna\.todos\.bridge\s*\n([\s\S]*?)\n\s*-->/);
50952
52579
  if (!match)
50953
52580
  return null;
50954
- const json3 = match[1].split(`
52581
+ const json4 = match[1].split(`
50955
52582
  `).map((line) => line.replace(/^ /, "")).join(`
50956
52583
  `);
50957
- return JSON.parse(json3);
52584
+ return JSON.parse(json4);
50958
52585
  }
50959
52586
  function frontmatterValue(markdown, key2) {
50960
52587
  const match = markdown.match(/^---\n([\s\S]*?)\n---/);
@@ -50975,14 +52602,14 @@ function parseTaskLine(line, currentPlan) {
50975
52602
  const match = line.match(/^\s*[-*]\s+\[([ xX])\]\s+(.+)$/);
50976
52603
  if (!match)
50977
52604
  return null;
50978
- const status2 = match[1].toLowerCase() === "x" ? "completed" : "pending";
52605
+ const status3 = match[1].toLowerCase() === "x" ? "completed" : "pending";
50979
52606
  const rawTitle = stripInlineMetadata(match[2]).replace(/^\*\*(.*?)\*\*$/, "$1");
50980
52607
  const tags = [...rawTitle.matchAll(/(^|\s)#([A-Za-z0-9_-]+)/g)].map((tag) => tag[2]);
50981
52608
  const assignee = rawTitle.match(/(^|\s)@([A-Za-z0-9_-]+)/)?.[2];
50982
52609
  const title = rawTitle.replace(/(^|\s)#[A-Za-z0-9_-]+/g, "").replace(/(^|\s)@[A-Za-z0-9_-]+/g, "").replace(/\s+/g, " ").trim();
50983
52610
  return {
50984
52611
  title,
50985
- status: status2,
52612
+ status: status3,
50986
52613
  priority: "medium",
50987
52614
  tags,
50988
52615
  assigned_to: assignee,
@@ -52587,7 +54214,7 @@ function normalizeJiraIssue(raw) {
52587
54214
  if (!summary)
52588
54215
  return null;
52589
54216
  const statusRec = asRecord3(fields.status);
52590
- const status2 = typeof statusRec?.name === "string" ? statusRec.name : undefined;
54217
+ const status3 = typeof statusRec?.name === "string" ? statusRec.name : undefined;
52591
54218
  const labels = labelNames2(fields.labels);
52592
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;
52593
54220
  return {
@@ -52596,13 +54223,13 @@ function normalizeJiraIssue(raw) {
52596
54223
  external_id: key2,
52597
54224
  title: `[${key2}] ${summary}`,
52598
54225
  description,
52599
- status: status2,
54226
+ status: status3,
52600
54227
  priority: priorityFromJira(fields.priority),
52601
54228
  tags: [...labels.slice(0, 10), "jira-import"],
52602
54229
  url: typeof raw.self === "string" ? raw.self : undefined,
52603
54230
  raw_metadata: {
52604
54231
  jira_key: key2,
52605
- jira_status: status2,
54232
+ jira_status: status3,
52606
54233
  jira_priority: asRecord3(fields.priority)?.name,
52607
54234
  jira_id: raw.id
52608
54235
  }
@@ -55211,21 +56838,22 @@ function upsertProject(raw, d) {
55211
56838
  const existing = getProject(id, d);
55212
56839
  const ts = now();
55213
56840
  if (!existing) {
55214
- d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at)
55215
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
56841
+ d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, parent_id, created_at, updated_at)
56842
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
55216
56843
  id,
55217
56844
  raw.name,
55218
56845
  raw.path,
55219
56846
  raw.description ?? null,
55220
- raw.task_list_id ?? `todos-${String(raw.name).toLowerCase().replace(/\s+/g, "-")}`,
56847
+ raw.task_list_id ?? String(raw.name).toLowerCase().replace(/\s+/g, "-"),
55221
56848
  raw.task_prefix ?? "TSK",
55222
56849
  raw.task_counter ?? 0,
56850
+ raw.parent_id ?? null,
55223
56851
  raw.created_at ?? ts,
55224
56852
  raw.updated_at ?? ts
55225
56853
  ].map(sqlValue));
55226
56854
  return "created";
55227
56855
  }
55228
- d.run(`UPDATE projects SET name = ?, path = ?, description = ?, task_list_id = ?, task_prefix = ?, task_counter = ?, updated_at = ?
56856
+ d.run(`UPDATE projects SET name = ?, path = ?, description = ?, task_list_id = ?, task_prefix = ?, task_counter = ?, parent_id = ?, updated_at = ?
55229
56857
  WHERE id = ?`, [
55230
56858
  raw.name ?? existing.name,
55231
56859
  raw.path ?? existing.path,
@@ -55233,6 +56861,7 @@ function upsertProject(raw, d) {
55233
56861
  raw.task_list_id ?? existing.task_list_id,
55234
56862
  raw.task_prefix ?? existing.task_prefix,
55235
56863
  raw.task_counter ?? existing.task_counter,
56864
+ raw.parent_id ?? existing.parent_id,
55236
56865
  raw.updated_at ?? ts,
55237
56866
  id
55238
56867
  ].map(sqlValue));
@@ -55313,7 +56942,7 @@ function importBundle(bundle, options = {}, db) {
55313
56942
  };
55314
56943
  if (options.dry_run)
55315
56944
  return result;
55316
- for (const raw of bundle.projects) {
56945
+ for (const raw of orderProjectsParentFirst(bundle.projects)) {
55317
56946
  try {
55318
56947
  const id = raw.id;
55319
56948
  const local = getProject(id, d);
@@ -55345,6 +56974,10 @@ function importBundle(bundle, options = {}, db) {
55345
56974
  }
55346
56975
  for (const dep of bundle.dependencies) {
55347
56976
  try {
56977
+ if (!getTask(dep.task_id, d) || !getTask(dep.depends_on, d)) {
56978
+ bump(result.skipped, "dependencies");
56979
+ continue;
56980
+ }
55348
56981
  d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [dep.task_id, dep.depends_on]);
55349
56982
  bump(result.created, "dependencies");
55350
56983
  } catch (e) {
@@ -56150,11 +57783,11 @@ function createTuiDashboardSnapshot(options = {}, db) {
56150
57783
  const d = db || getDatabase();
56151
57784
  const limit = limited(options.limit);
56152
57785
  const filter = projectFilter(options.project_id);
56153
- const counts = TASK_STATUSES2.reduce((acc, status2) => {
56154
- acc[status2] = countTasks({ ...filter, status: status2 }, d);
57786
+ const counts = TASK_STATUSES2.reduce((acc, status3) => {
57787
+ acc[status3] = countTasks({ ...filter, status: status3 }, d);
56155
57788
  return acc;
56156
57789
  }, { total: 0 });
56157
- counts.total = TASK_STATUSES2.reduce((sum, status2) => sum + counts[status2], 0);
57790
+ counts.total = TASK_STATUSES2.reduce((sum, status3) => sum + counts[status3], 0);
56158
57791
  const projectRows = listProjects(d).filter((project) => !options.project_id || project.id === options.project_id).slice(0, limit).map((project) => ({
56159
57792
  id: project.id,
56160
57793
  name: project.name,
@@ -57355,12 +58988,12 @@ function captureGit(root, warnings) {
57355
58988
  }
57356
58989
  const branch = runLocalCommand(root, ["git", "branch", "--show-current"]);
57357
58990
  const commit = runLocalCommand(root, ["git", "rev-parse", "HEAD"]);
57358
- const status2 = runLocalCommand(root, ["git", "status", "--porcelain=v1"]);
58991
+ const status3 = runLocalCommand(root, ["git", "status", "--porcelain=v1"]);
57359
58992
  if (commit.exitCode !== 0)
57360
58993
  warnings.push(`git commit unavailable: ${commit.stderr || commit.stdout || "unknown error"}`);
57361
- if (status2.exitCode !== 0)
57362
- warnings.push(`git status unavailable: ${status2.stderr || status2.stdout || "unknown error"}`);
57363
- 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) : [];
57364
58997
  return {
57365
58998
  present: true,
57366
58999
  branch: branch.stdout || null,
@@ -57402,8 +59035,8 @@ function defaultSnapshotDir() {
57402
59035
  return join18(dirname15(resolve17(dbPath)), "environment-snapshots");
57403
59036
  }
57404
59037
  function snapshotWithId(snapshot) {
57405
- const digest2 = sha2566(JSON.stringify(snapshot)).slice(0, 24);
57406
- return { id: `env_${digest2}`, ...snapshot };
59038
+ const digest3 = sha2566(JSON.stringify(snapshot)).slice(0, 24);
59039
+ return { id: `env_${digest3}`, ...snapshot };
57407
59040
  }
57408
59041
  function captureEnvironmentSnapshot(input = {}) {
57409
59042
  const root = resolve17(input.root || process.cwd());
@@ -57603,7 +59236,7 @@ function createDecisionRecord(input, db) {
57603
59236
  const ts = now();
57604
59237
  const sequenceNum = nextSequenceNum(input.project_id, d);
57605
59238
  const shortRef = buildShortRef(input.project_id, sequenceNum, d);
57606
- const status2 = input.status ?? "proposed";
59239
+ const status3 = input.status ?? "proposed";
57607
59240
  d.run(`INSERT INTO decision_records (
57608
59241
  id, project_id, task_id, plan_id, agent_id, sequence_num, short_ref,
57609
59242
  title, status, context, decision, consequences, alternatives, tags,
@@ -57617,7 +59250,7 @@ function createDecisionRecord(input, db) {
57617
59250
  sequenceNum,
57618
59251
  shortRef,
57619
59252
  input.title,
57620
- status2,
59253
+ status3,
57621
59254
  input.context ?? null,
57622
59255
  input.decision,
57623
59256
  input.consequences ?? null,
@@ -57695,15 +59328,15 @@ function updateDecisionRecord(id, input, db) {
57695
59328
  ]);
57696
59329
  return getDecisionRecord(id, d);
57697
59330
  }
57698
- function setDecisionStatus(id, status2, db) {
57699
- if (!DECISION_STATUSES.includes(status2)) {
57700
- 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}`);
57701
59334
  }
57702
59335
  const d = db || getDatabase();
57703
59336
  const existing = getDecisionRecord(id, d);
57704
59337
  if (!existing)
57705
59338
  throw new Error(`Decision record not found: ${id}`);
57706
- 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]);
57707
59340
  return getDecisionRecord(id, d);
57708
59341
  }
57709
59342
  function supersedeDecisionRecord(id, input, db) {
@@ -57984,17 +59617,17 @@ function buildReportExportData(input, db) {
57984
59617
  throw new Error("Project not found");
57985
59618
  projectId = project.id;
57986
59619
  title = `Project: ${project.name}`;
57987
- const status2 = getStatus({ project_id: project.id }, undefined, undefined, d);
59620
+ const status3 = getStatus({ project_id: project.id }, undefined, undefined, d);
57988
59621
  sections.push(section("overview", "Overview", [
57989
59622
  `Name: ${project.name}`,
57990
59623
  `Path: ${project.path}`,
57991
59624
  `Prefix: ${project.task_prefix ?? "\u2014"}`
57992
59625
  ]), section("status", "Status", [
57993
- `Pending: ${status2.pending}`,
57994
- `In progress: ${status2.in_progress}`,
57995
- `Completed: ${status2.completed}`,
57996
- `Total: ${status2.total}`,
57997
- `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}`
57998
59631
  ]));
57999
59632
  const tasks = listTasks({ project_id: project.id, limit: 50 }, d);
58000
59633
  sections.push(section("tasks", "Recent tasks", tasks.length ? tasks.map(taskLine2) : ["No tasks"]));
@@ -58815,8 +60448,8 @@ function getGitStatus(root, branch, includeGitStatus) {
58815
60448
  return { has_git: false, current_branch: null, branch_exists: false, dirty_files: [] };
58816
60449
  const currentBranch = runGit3(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
58817
60450
  const branchExists = runGit3(root, ["show-ref", "--verify", `refs/heads/${branch}`]) !== null;
58818
- const status2 = runGit3(root, ["status", "--short"]) || "";
58819
- const dirtyFiles = status2.split(`
60451
+ const status3 = runGit3(root, ["status", "--short"]) || "";
60452
+ const dirtyFiles = status3.split(`
58820
60453
  `).map((line) => line.trim()).filter(Boolean).map((line) => normalizePath6(line.replace(/^.. /, "").replace(/^.* -> /, ""))).filter((path) => Boolean(path));
58821
60454
  return { has_git: true, current_branch: currentBranch || null, branch_exists: branchExists, dirty_files: dirtyFiles };
58822
60455
  }
@@ -59214,8 +60847,8 @@ var AGENT_WORKFLOW_DEMO_SCHEMA = "todos.agent_workflow_demo.v1";
59214
60847
  var DEMO_DEFAULT_AGENT = "demoagent";
59215
60848
  var DEMO_DEFAULT_PROJECT = "Agent Workflow Demo";
59216
60849
  var DEMO_PROJECT_PATH = "/tmp/todos-agent-workflow-demo";
59217
- function pushStep(steps, action, detail, status2 = "ok") {
59218
- 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 });
59219
60852
  }
59220
60853
  function setupEphemeralDemoDb(options = {}) {
59221
60854
  const prevPath = process.env["TODOS_DB_PATH"];
@@ -59342,7 +60975,7 @@ function runAgentWorkflowDemo(options = {}) {
59342
60975
  pushStep(steps, "complete_run_record", `Run record status=${finishedRecord.status}`);
59343
60976
  completeTask(third.id, agent.id, d, { notes: "Demo: finished via agent run + run record" });
59344
60977
  pushStep(steps, "complete_task", `Completed ${third.short_id ?? third.id.slice(0, 8)}`);
59345
- const status2 = getStatus({ project_id: project.id }, agent.id, undefined, d);
60978
+ const status3 = getStatus({ project_id: project.id }, agent.id, undefined, d);
59346
60979
  const tasks = listTasks({ project_id: project.id }, d).map((t) => ({
59347
60980
  id: t.id,
59348
60981
  short_id: t.short_id,
@@ -59374,14 +61007,14 @@ function runAgentWorkflowDemo(options = {}) {
59374
61007
  objective: finishedRecord.objective
59375
61008
  },
59376
61009
  status_summary: {
59377
- pending: status2.pending,
59378
- in_progress: status2.in_progress,
59379
- completed: status2.completed,
59380
- total: status2.total
61010
+ pending: status3.pending,
61011
+ in_progress: status3.in_progress,
61012
+ completed: status3.completed,
61013
+ total: status3.total
59381
61014
  },
59382
61015
  steps,
59383
61016
  completed_at: exportedAt2,
59384
- 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`
59385
61018
  };
59386
61019
  } finally {
59387
61020
  if (dbHandle)
@@ -60355,8 +61988,8 @@ function rowToOrg(row) {
60355
61988
  function createOrg(input, db) {
60356
61989
  const d = db || getDatabase();
60357
61990
  const id = uuid();
60358
- const timestamp4 = now();
60359
- 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]);
60360
61993
  return getOrg(id, d);
60361
61994
  }
60362
61995
  function getOrg(id, db) {
@@ -60604,10 +62237,10 @@ function getCriticalPath2(opts, db) {
60604
62237
  function addKgEdge(sourceId, sourceType, targetId, targetType, relationType, weight = 1, metadata, db) {
60605
62238
  const d = db || getDatabase();
60606
62239
  const id = uuid();
60607
- const timestamp4 = now();
62240
+ const timestamp5 = now();
60608
62241
  d.run(`INSERT OR IGNORE INTO kg_edges (id, source_id, source_type, target_id, target_type, relation_type, weight, metadata, created_at)
60609
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, sourceId, sourceType, targetId, targetType, relationType, weight, JSON.stringify(metadata || {}), timestamp4]);
60610
- 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 };
60611
62244
  }
60612
62245
  function removeKgEdges(sourceId, targetId, relationType, db) {
60613
62246
  const d = db || getDatabase();
@@ -61507,7 +63140,7 @@ function runSavedSearch(filters = {}, scope = "tasks", db) {
61507
63140
  function saveSearchView(input, db) {
61508
63141
  const d = db || getDatabase();
61509
63142
  const name = normalizeName3(input.name);
61510
- const timestamp4 = now();
63143
+ const timestamp5 = now();
61511
63144
  const existing = getSearchView(name, d);
61512
63145
  if (existing) {
61513
63146
  d.run(`UPDATE saved_search_views
@@ -61516,7 +63149,7 @@ function saveSearchView(input, db) {
61516
63149
  input.description ?? existing.description,
61517
63150
  normalizeScope(input.scope ?? existing.scope),
61518
63151
  JSON.stringify(input.filters ?? existing.filters),
61519
- timestamp4,
63152
+ timestamp5,
61520
63153
  existing.id
61521
63154
  ]);
61522
63155
  return getSearchView(existing.id, d);
@@ -61529,8 +63162,8 @@ function saveSearchView(input, db) {
61529
63162
  input.description ?? null,
61530
63163
  normalizeScope(input.scope),
61531
63164
  JSON.stringify(input.filters ?? {}),
61532
- timestamp4,
61533
- timestamp4
63165
+ timestamp5,
63166
+ timestamp5
61534
63167
  ]);
61535
63168
  return getSearchView(id, d);
61536
63169
  }
@@ -61572,14 +63205,14 @@ function readClaudeTask(dir, filename) {
61572
63205
  function writeClaudeTask(dir, task3) {
61573
63206
  writeJsonFile(join23(dir, `${task3.id}.json`), task3);
61574
63207
  }
61575
- function toClaudeStatus(status2) {
61576
- if (status2 === "pending" || status2 === "in_progress" || status2 === "completed") {
61577
- return status2;
63208
+ function toClaudeStatus(status3) {
63209
+ if (status3 === "pending" || status3 === "in_progress" || status3 === "completed") {
63210
+ return status3;
61578
63211
  }
61579
63212
  return "completed";
61580
63213
  }
61581
- function toSqliteStatus(status2) {
61582
- return status2;
63214
+ function toSqliteStatus(status3) {
63215
+ return status3;
61583
63216
  }
61584
63217
  function readPrefixCounter(dir) {
61585
63218
  const path = join23(dir, ".prefix-counter");
@@ -62540,7 +64173,7 @@ init_database();
62540
64173
  function logTrace(input, db) {
62541
64174
  const d = db || getDatabase();
62542
64175
  const id = uuid();
62543
- const timestamp4 = now();
64176
+ const timestamp5 = now();
62544
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)
62545
64178
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
62546
64179
  id,
@@ -62553,7 +64186,7 @@ function logTrace(input, db) {
62553
64186
  input.duration_ms ?? null,
62554
64187
  input.tokens ?? null,
62555
64188
  input.cost_usd ?? null,
62556
- timestamp4
64189
+ timestamp5
62557
64190
  ]);
62558
64191
  return {
62559
64192
  id,
@@ -62566,7 +64199,7 @@ function logTrace(input, db) {
62566
64199
  duration_ms: input.duration_ms ?? null,
62567
64200
  tokens: input.tokens ?? null,
62568
64201
  cost_usd: input.cost_usd ?? null,
62569
- created_at: timestamp4
64202
+ created_at: timestamp5
62570
64203
  };
62571
64204
  }
62572
64205
  function getTaskTraces(taskId, db) {
@@ -62590,7 +64223,7 @@ init_database();
62590
64223
  init_tasks();
62591
64224
  function setBudget(agentId, opts, db) {
62592
64225
  const d = db || getDatabase();
62593
- const timestamp4 = now();
64226
+ const timestamp5 = now();
62594
64227
  d.run(`INSERT INTO agent_budgets (agent_id, max_concurrent, max_cost_usd, max_task_minutes, period_hours, created_at, updated_at)
62595
64228
  VALUES (?, ?, ?, ?, ?, ?, ?)
62596
64229
  ON CONFLICT(agent_id) DO UPDATE SET
@@ -62604,13 +64237,13 @@ function setBudget(agentId, opts, db) {
62604
64237
  opts.max_cost_usd ?? null,
62605
64238
  opts.max_task_minutes ?? null,
62606
64239
  opts.period_hours ?? 24,
62607
- timestamp4,
62608
- timestamp4,
64240
+ timestamp5,
64241
+ timestamp5,
62609
64242
  opts.max_concurrent ?? null,
62610
64243
  opts.max_cost_usd ?? null,
62611
64244
  opts.max_task_minutes ?? null,
62612
64245
  opts.period_hours ?? null,
62613
- timestamp4
64246
+ timestamp5
62614
64247
  ]);
62615
64248
  return getBudget(agentId, d);
62616
64249
  }
@@ -62913,11 +64546,11 @@ function resolveWorkflowState(input, projectPath) {
62913
64546
  return { input, state: byCanonical, matched_by: "canonical_status" };
62914
64547
  throw new Error(`Unknown workflow state: ${input}`);
62915
64548
  }
62916
- function stateForCanonicalStatus(status2, projectPath) {
62917
- 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));
62918
64551
  if (configured)
62919
64552
  return configured;
62920
- 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;
62921
64554
  }
62922
64555
  function getTaskWorkflowState(taskId, db, projectPath) {
62923
64556
  const d = db || getDatabase();
@@ -63081,9 +64714,9 @@ function unpackFixture(input) {
63081
64714
  const contextPack = input["context_pack"];
63082
64715
  return isObject(contextPack) ? contextPack : input;
63083
64716
  }
63084
- function approvalStatus(status2) {
63085
- if (status2 === "approved" || status2 === "rejected" || status2 === "expired")
63086
- return status2;
64717
+ function approvalStatus(status3) {
64718
+ if (status3 === "approved" || status3 === "rejected" || status3 === "expired")
64719
+ return status3;
63087
64720
  return "pending";
63088
64721
  }
63089
64722
  function approvalsFromFixture(fixture, runs) {
@@ -63180,17 +64813,17 @@ function simulateAgentReplay(input, options = {}) {
63180
64813
  if (["completed", "failed", "cancelled"].includes(runStatus))
63181
64814
  currentStatus = runStatus;
63182
64815
  for (const command of asArray2(run["commands"])) {
63183
- const status2 = normalizedStatus(command["status"]);
64816
+ const status3 = normalizedStatus(command["status"]);
63184
64817
  const commandText = text(command["command"]) || "command";
63185
64818
  push({
63186
64819
  type: "command",
63187
64820
  source: "run.commands",
63188
64821
  label: commandText,
63189
- status: status2,
64822
+ status: status3,
63190
64823
  command: commandText,
63191
64824
  message: text(command["output_summary"])
63192
64825
  });
63193
- if (status2 === "failed")
64826
+ if (status3 === "failed")
63194
64827
  failures.push({ source: "run.commands", message: text(command["output_summary"]) || "command failed", command: commandText });
63195
64828
  }
63196
64829
  for (const file of asArray2(run["files"])) {
@@ -63207,17 +64840,17 @@ function simulateAgentReplay(input, options = {}) {
63207
64840
  }
63208
64841
  }
63209
64842
  for (const verification2 of verifications) {
63210
- const status2 = normalizedStatus(verification2["status"]);
64843
+ const status3 = normalizedStatus(verification2["status"]);
63211
64844
  const commandText = text(verification2["command"]) || "verification";
63212
64845
  push({
63213
64846
  type: "command",
63214
64847
  source: "traceability.verifications",
63215
64848
  label: commandText,
63216
- status: status2,
64849
+ status: status3,
63217
64850
  command: commandText,
63218
64851
  message: text(verification2["output_summary"])
63219
64852
  });
63220
- if (status2 === "failed")
64853
+ if (status3 === "failed")
63221
64854
  failures.push({ source: "traceability.verifications", message: text(verification2["output_summary"]) || "verification failed", command: commandText });
63222
64855
  }
63223
64856
  const approvals = approvalsFromFixture(fixture, runs);
@@ -63753,7 +65386,7 @@ function installLocalExtension(input) {
63753
65386
  const validation = inspected.validation;
63754
65387
  if (!validation.ok)
63755
65388
  throw new Error(`extension manifest invalid: ${validation.errors.join("; ")}`);
63756
- const timestamp4 = new Date().toISOString();
65389
+ const timestamp5 = new Date().toISOString();
63757
65390
  const config = loadConfig();
63758
65391
  const existing = config.extension_registry?.[inspected.manifest.name];
63759
65392
  const trusted = Boolean(input.trust);
@@ -63773,8 +65406,8 @@ function installLocalExtension(input) {
63773
65406
  ...trusted ? [] : ["extension installed as needs_review until trusted explicitly"]
63774
65407
  ],
63775
65408
  diagnostics: testExtensionCompatibility(inspected.source),
63776
- installed_at: existing?.installed_at || timestamp4,
63777
- updated_at: timestamp4
65409
+ installed_at: existing?.installed_at || timestamp5,
65410
+ updated_at: timestamp5
63778
65411
  };
63779
65412
  saveConfig({
63780
65413
  ...config,
@@ -64136,9 +65769,9 @@ async function sendToTmux(target, message, delayMs, options = false) {
64136
65769
  ensureTmuxAvailable();
64137
65770
  if (!opts.confirmBusy) {
64138
65771
  const pane = await inspectTmuxPane(target);
64139
- const status2 = tmuxPaneBusyStatus(pane);
64140
- if (status2.busy) {
64141
- 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.`);
64142
65775
  }
64143
65776
  }
64144
65777
  const sendProc = Bun.spawn(["tmux", "send-keys", "-t", target, message, ""], {
@@ -64283,10 +65916,10 @@ function normalizeStatus(value) {
64283
65916
  return "open";
64284
65917
  }
64285
65918
  function normalizeResolutionStatus(value) {
64286
- const status2 = normalizeStatus(value || "resolved");
64287
- if (status2 === "open")
65919
+ const status3 = normalizeStatus(value || "resolved");
65920
+ if (status3 === "open")
64288
65921
  throw new Error("resolve-missing status must be resolved or ignored");
64289
- return status2;
65922
+ return status3;
64290
65923
  }
64291
65924
  function redactOptional(value, max = 2000) {
64292
65925
  if (!value)
@@ -64324,7 +65957,7 @@ function compactFinding(finding2) {
64324
65957
  metadata_keys: Object.keys(finding2.metadata).sort()
64325
65958
  };
64326
65959
  }
64327
- function previewFinding(existing, next, timestamp4) {
65960
+ function previewFinding(existing, next, timestamp5) {
64328
65961
  return {
64329
65962
  ...existing,
64330
65963
  run_id: next.run_id,
@@ -64335,9 +65968,9 @@ function previewFinding(existing, next, timestamp4) {
64335
65968
  summary: next.summary,
64336
65969
  artifact_path: next.artifact_path,
64337
65970
  metadata: next.metadata,
64338
- last_seen_at: timestamp4,
64339
- resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp4,
64340
- updated_at: timestamp4
65971
+ last_seen_at: timestamp5,
65972
+ resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp5,
65973
+ updated_at: timestamp5
64341
65974
  };
64342
65975
  }
64343
65976
  function upsertAction(existing, next) {
@@ -64387,7 +66020,7 @@ function sameFinding(left, right) {
64387
66020
  function upsertTaskFinding(input, db) {
64388
66021
  const d = db || getDatabase();
64389
66022
  assertTask(input.task_id, d);
64390
- const timestamp4 = input.observed_at || now();
66023
+ const timestamp5 = input.observed_at || now();
64391
66024
  const warnings = [];
64392
66025
  const next = nextFinding(input, d);
64393
66026
  const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
@@ -64398,10 +66031,10 @@ function upsertTaskFinding(input, db) {
64398
66031
  schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
64399
66032
  local_only: true,
64400
66033
  dry_run: true,
64401
- processed_at: timestamp4,
66034
+ processed_at: timestamp5,
64402
66035
  action: action2,
64403
66036
  fingerprint: next.fingerprint,
64404
- finding: existing ? compactFinding(previewFinding(existing, next, timestamp4)) : null,
66037
+ finding: existing ? compactFinding(previewFinding(existing, next, timestamp5)) : null,
64405
66038
  warnings
64406
66039
  };
64407
66040
  }
@@ -64422,17 +66055,17 @@ function upsertTaskFinding(input, db) {
64422
66055
  next.summary,
64423
66056
  next.artifact_path,
64424
66057
  JSON.stringify(next.metadata),
64425
- timestamp4,
64426
- timestamp4,
64427
- next.status === "open" ? null : timestamp4,
64428
- timestamp4,
64429
- timestamp4
66058
+ timestamp5,
66059
+ timestamp5,
66060
+ next.status === "open" ? null : timestamp5,
66061
+ timestamp5,
66062
+ timestamp5
64430
66063
  ]);
64431
66064
  return {
64432
66065
  schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
64433
66066
  local_only: true,
64434
66067
  dry_run: false,
64435
- processed_at: timestamp4,
66068
+ processed_at: timestamp5,
64436
66069
  action: "created",
64437
66070
  fingerprint: next.fingerprint,
64438
66071
  finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
@@ -64444,7 +66077,7 @@ function upsertTaskFinding(input, db) {
64444
66077
  schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
64445
66078
  local_only: true,
64446
66079
  dry_run: false,
64447
- processed_at: timestamp4,
66080
+ processed_at: timestamp5,
64448
66081
  action: "matched",
64449
66082
  fingerprint: next.fingerprint,
64450
66083
  finding: compactFinding(existing),
@@ -64464,16 +66097,16 @@ function upsertTaskFinding(input, db) {
64464
66097
  next.summary,
64465
66098
  next.artifact_path,
64466
66099
  JSON.stringify(next.metadata),
64467
- timestamp4,
64468
- next.status === "open" ? null : existing.resolved_at || timestamp4,
64469
- timestamp4,
66100
+ timestamp5,
66101
+ next.status === "open" ? null : existing.resolved_at || timestamp5,
66102
+ timestamp5,
64470
66103
  existing.id
64471
66104
  ]);
64472
66105
  return {
64473
66106
  schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
64474
66107
  local_only: true,
64475
66108
  dry_run: false,
64476
- processed_at: timestamp4,
66109
+ processed_at: timestamp5,
64477
66110
  action,
64478
66111
  fingerprint: next.fingerprint,
64479
66112
  finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
@@ -64510,8 +66143,8 @@ function listCompactTaskFindings(filter = {}, db) {
64510
66143
  function resolveMissingTaskFindings(input, db) {
64511
66144
  const d = db || getDatabase();
64512
66145
  assertTask(input.task_id, d);
64513
- const timestamp4 = input.resolved_at || now();
64514
- const status2 = normalizeResolutionStatus(input.status);
66146
+ const timestamp5 = input.resolved_at || now();
66147
+ const status3 = normalizeResolutionStatus(input.status);
64515
66148
  const runId = resolveRunForTask(input.run_id, input.task_id, d);
64516
66149
  const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
64517
66150
  const warnings = [];
@@ -64530,7 +66163,7 @@ function resolveMissingTaskFindings(input, db) {
64530
66163
  schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
64531
66164
  local_only: true,
64532
66165
  dry_run: true,
64533
- processed_at: timestamp4,
66166
+ processed_at: timestamp5,
64534
66167
  action: candidates.length > 0 ? "preview" : "noop",
64535
66168
  task_id: input.task_id,
64536
66169
  source: input.source ? redactOptional(input.source, 120) : null,
@@ -64553,10 +66186,10 @@ function resolveMissingTaskFindings(input, db) {
64553
66186
  const tx = d.transaction(() => {
64554
66187
  for (const finding2 of candidates) {
64555
66188
  d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
64556
- status2,
64557
- timestamp4,
66189
+ status3,
66190
+ timestamp5,
64558
66191
  JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
64559
- timestamp4,
66192
+ timestamp5,
64560
66193
  finding2.id
64561
66194
  ]);
64562
66195
  }
@@ -64568,8 +66201,8 @@ function resolveMissingTaskFindings(input, db) {
64568
66201
  schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
64569
66202
  local_only: true,
64570
66203
  dry_run: false,
64571
- processed_at: timestamp4,
64572
- action: updated.length > 0 ? status2 : "noop",
66204
+ processed_at: timestamp5,
66205
+ action: updated.length > 0 ? status3 : "noop",
64573
66206
  task_id: input.task_id,
64574
66207
  source: input.source ? redactOptional(input.source, 120) : null,
64575
66208
  run_id: runId,
@@ -65462,7 +67095,7 @@ init_tasks();
65462
67095
  init_task_relations();
65463
67096
  init_config2();
65464
67097
  var LOCAL_CAPACITY_SCHEMA_VERSION = 1;
65465
- function timestamp4() {
67098
+ function timestamp5() {
65466
67099
  return new Date().toISOString();
65467
67100
  }
65468
67101
  function todayIso() {
@@ -65546,7 +67179,7 @@ function upsertCapacityProfile(input) {
65546
67179
  const store = readStore2();
65547
67180
  const key2 = profileKey(agentId, projectId);
65548
67181
  const existing = store.profiles[key2];
65549
- const now4 = timestamp4();
67182
+ const now4 = timestamp5();
65550
67183
  const profile = {
65551
67184
  id: existing?.id ?? key2,
65552
67185
  agent_id: agentId,
@@ -65627,7 +67260,7 @@ function getPlanningForecast(input = {}, db) {
65627
67260
  return {
65628
67261
  schema_version: LOCAL_CAPACITY_SCHEMA_VERSION,
65629
67262
  local_only: true,
65630
- generated_at: timestamp4(),
67263
+ generated_at: timestamp5(),
65631
67264
  filters: {
65632
67265
  project_id: input.project_id ?? null,
65633
67266
  plan_id: input.plan_id ?? null,
@@ -66015,6 +67648,8 @@ export {
66015
67648
  testTerminalNotificationRule,
66016
67649
  testLocalEventHook,
66017
67650
  tasksFromTemplate,
67651
+ taskSubtreeTransferRollbackRequestDigest,
67652
+ taskSubtreeTransferRequestDigest,
66018
67653
  taskManifestRequestDigest,
66019
67654
  taskManifestCompensationRequestDigest,
66020
67655
  taskFromTemplate,
@@ -66037,6 +67672,7 @@ export {
66037
67672
  startTaskRun,
66038
67673
  startTask,
66039
67674
  startFocusSession,
67675
+ sqliteTodosTaskSubtreeTransferSchemaSql,
66040
67676
  sqliteTodosTaskManifestSchemaSql,
66041
67677
  sqliteTodosProjectRegistrationSchemaSql,
66042
67678
  softDeleteArtifact,
@@ -66228,6 +67864,7 @@ export {
66228
67864
  previewInstalledTemplate,
66229
67865
  previewInboxIntake,
66230
67866
  previewBuiltinTemplate,
67867
+ postgresTodosTaskSubtreeTransferSchemaSql,
66231
67868
  postgresTodosTaskManifestSchemaSql,
66232
67869
  postgresTodosSyncSchemaSql,
66233
67870
  postgresTodosProjectRegistrationSchemaSql,
@@ -66236,6 +67873,9 @@ export {
66236
67873
  planPlanProjectLink,
66237
67874
  pauseFocusSession,
66238
67875
  patrolTasks,
67876
+ parseTodosTaskSubtreeTransferRollback,
67877
+ parseTodosTaskSubtreeTransferInspect,
67878
+ parseTodosTaskSubtreeTransferApply,
66239
67879
  parseTodosTaskManifestCompensation,
66240
67880
  parseTodosTaskManifestBindingLookup,
66241
67881
  parseTodosTaskManifest,
@@ -66243,7 +67883,6 @@ export {
66243
67883
  parseTodosAiOutputSchema,
66244
67884
  parseTodosAiJson,
66245
67885
  parseTmuxTarget,
66246
- parseStorageMode,
66247
67886
  parseStorageBackend,
66248
67887
  parseRecurrenceRule,
66249
67888
  parseQuietHours,
@@ -66394,7 +68033,6 @@ export {
66394
68033
  issueToTask,
66395
68034
  isWorktreePath,
66396
68035
  isValidRecurrenceRule,
66397
- isTodosRemoteStorageEnabled,
66398
68036
  isTodosPostgresBackend,
66399
68037
  isTodosAiUpdateTaskResult,
66400
68038
  isTodosAiRuntimeEvent,
@@ -66434,6 +68072,7 @@ export {
66434
68072
  importActivityLog,
66435
68073
  hasSecretFindings,
66436
68074
  hasActiveApiKeys,
68075
+ handleTodosTaskSubtreeTransferHttpRequest,
66437
68076
  handleTodosTaskManifestHttpRequest,
66438
68077
  handleTodosProjectRegistrationHttpRequest,
66439
68078
  getWorkspaceTrustStatus,
@@ -66447,7 +68086,6 @@ export {
66447
68086
  getUnlockImpact,
66448
68087
  getTraceStats,
66449
68088
  getTopologyDocs,
66450
- getTodosStorageMode,
66451
68089
  getTodosStorageEnvName,
66452
68090
  getTodosStorageDatabaseUrl,
66453
68091
  getTodosStorageDatabaseEnv,
@@ -66492,7 +68130,6 @@ export {
66492
68130
  getTaskBoard,
66493
68131
  getTask,
66494
68132
  getStoredHandoffAsPacket,
66495
- getStorageMode,
66496
68133
  getStorageDatabaseUrl,
66497
68134
  getStorageDatabaseEnv,
66498
68135
  getStatus,
@@ -66751,6 +68388,9 @@ export {
66751
68388
  detectInboxSourceType,
66752
68389
  detectCyclesFromEdges,
66753
68390
  describeTerminalNotificationRule,
68391
+ deriveTodosTaskSubtreeTransferRollbackPreconditionDigest,
68392
+ deriveTodosTaskSubtreeTransferIdempotencyKey,
68393
+ deriveTodosTaskSubtreeTransferApplyPreconditionDigest,
66754
68394
  deriveTodosTaskManifestIdempotencyKey,
66755
68395
  deriveTodosTaskManifestCompensationPreconditionDigest,
66756
68396
  deriveTodosTaskManifestApplyPreconditionDigest,
@@ -66787,6 +68427,7 @@ export {
66787
68427
  createVerificationEvidence,
66788
68428
  createUserScaffold,
66789
68429
  createTuiDashboardSnapshot,
68430
+ createTodosTaskSubtreeTransferHttpClient,
66790
68431
  createTodosTaskManifestHttpClient,
66791
68432
  createTodosStorageAdapter,
66792
68433
  createTodosS3ArtifactStore,
@@ -66801,6 +68442,7 @@ export {
66801
68442
  createTaskList,
66802
68443
  createTaskBoard,
66803
68444
  createTask,
68445
+ createSqliteTodosTaskSubtreeTransferAuthority,
66804
68446
  createSqliteTodosTaskManifestAuthority,
66805
68447
  createSessionRecoveryHandoff,
66806
68448
  createSession,
@@ -66816,6 +68458,7 @@ export {
66816
68458
  createReminder,
66817
68459
  createReleaseCompatibilityReport,
66818
68460
  createProject,
68461
+ createPostgresTodosTaskSubtreeTransferAuthority,
66819
68462
  createPostgresTodosTaskManifestAuthority,
66820
68463
  createPostgresTodosSyncStore,
66821
68464
  createPostgresTodosStorageAdapter,
@@ -66984,6 +68627,8 @@ export {
66984
68627
  VersionConflictError,
66985
68628
  VERIFICATION_EVIDENCE_SCHEMA,
66986
68629
  USER_SCAFFOLD_SCHEMA,
68630
+ TodosTaskSubtreeTransferHttpClient,
68631
+ TodosTaskSubtreeTransferError,
66987
68632
  TodosTaskManifestHttpClient,
66988
68633
  TodosTaskManifestError,
66989
68634
  TodosProjectRegistrationHttpClient,
@@ -66995,6 +68640,8 @@ export {
66995
68640
  TaskNotFoundError,
66996
68641
  TaskListNotFoundError,
66997
68642
  TUI_DASHBOARD_VIEWS,
68643
+ TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION,
68644
+ TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
66998
68645
  TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
66999
68646
  TODOS_TASK_MANIFEST_SCHEMA_VERSION,
67000
68647
  TODOS_TASK_MANIFEST_ROUTE,
@@ -67101,6 +68748,7 @@ export {
67101
68748
  PostgresTodosProjectRegistrationBackend,
67102
68749
  PlanProjectLinkError,
67103
68750
  PlanNotFoundError,
68751
+ PackageOwnedTodosTaskSubtreeTransferAuthority,
67104
68752
  PackageOwnedTodosTaskManifestAuthority,
67105
68753
  PackageOwnedTodosProjectRegistrationAuthority,
67106
68754
  PROJECT_DEPENDENCY_GRAPH_SCHEMA,