@hasna/todos 0.15.35 → 0.15.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/cli/cloud-router.d.ts +52 -18
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/agent-commands.d.ts.map +1 -1
  4. package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  7. package/dist/cli/commands/task-subtree-transfer-commands.d.ts +3 -0
  8. package/dist/cli/commands/task-subtree-transfer-commands.d.ts.map +1 -0
  9. package/dist/cli/index.js +3299 -796
  10. package/dist/cli/stage-a.d.ts.map +1 -1
  11. package/dist/contracts.js +62 -54
  12. package/dist/db/plan-comments.d.ts +14 -0
  13. package/dist/db/plan-comments.d.ts.map +1 -0
  14. package/dist/db/schema.d.ts.map +1 -1
  15. package/dist/db/task-crud.d.ts.map +1 -1
  16. package/dist/db/task-lifecycle.d.ts.map +1 -1
  17. package/dist/db/task-lists.d.ts.map +1 -1
  18. package/dist/db/task-relations.d.ts.map +1 -1
  19. package/dist/index.d.ts +3 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1787 -309
  22. package/dist/lib/comment-cursor.d.ts +19 -0
  23. package/dist/lib/comment-cursor.d.ts.map +1 -1
  24. package/dist/lib/import-export-bridge.d.ts.map +1 -1
  25. package/dist/mcp/index.js +2273 -471
  26. package/dist/mcp.js +8 -4
  27. package/dist/project-registration.js +207 -63
  28. package/dist/registry.d.ts +1 -1
  29. package/dist/registry.d.ts.map +1 -1
  30. package/dist/registry.js +69 -54
  31. package/dist/release-provenance.json +5 -5
  32. package/dist/sdk/index.d.ts +1 -1
  33. package/dist/sdk/index.d.ts.map +1 -1
  34. package/dist/sdk/index.js +35 -0
  35. package/dist/sdk/v1.generated.d.ts +141 -0
  36. package/dist/sdk/v1.generated.d.ts.map +1 -1
  37. package/dist/server/cloud.d.ts +12 -2
  38. package/dist/server/cloud.d.ts.map +1 -1
  39. package/dist/server/index.js +5171 -8535
  40. package/dist/server/openapi.d.ts +2714 -1937
  41. package/dist/server/openapi.d.ts.map +1 -1
  42. package/dist/server/v1.d.ts +2 -1
  43. package/dist/server/v1.d.ts.map +1 -1
  44. package/dist/storage/config.d.ts +12 -30
  45. package/dist/storage/config.d.ts.map +1 -1
  46. package/dist/storage/index.d.ts +2 -2
  47. package/dist/storage/index.d.ts.map +1 -1
  48. package/dist/storage/interfaces.d.ts +13 -2
  49. package/dist/storage/interfaces.d.ts.map +1 -1
  50. package/dist/storage/local-sqlite.d.ts.map +1 -1
  51. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  52. package/dist/storage/timestamp-backfill.d.ts +62 -0
  53. package/dist/storage/timestamp-backfill.d.ts.map +1 -0
  54. package/dist/storage.d.ts +2 -2
  55. package/dist/storage.d.ts.map +1 -1
  56. package/dist/storage.js +199 -63
  57. package/dist/task-subtree-transfer/authority.d.ts +27 -0
  58. package/dist/task-subtree-transfer/authority.d.ts.map +1 -0
  59. package/dist/task-subtree-transfer/backend.d.ts +9 -0
  60. package/dist/task-subtree-transfer/backend.d.ts.map +1 -0
  61. package/dist/task-subtree-transfer/canonical.d.ts +14 -0
  62. package/dist/task-subtree-transfer/canonical.d.ts.map +1 -0
  63. package/dist/task-subtree-transfer/http.d.ts +16 -0
  64. package/dist/task-subtree-transfer/http.d.ts.map +1 -0
  65. package/dist/task-subtree-transfer/index.d.ts +6 -0
  66. package/dist/task-subtree-transfer/index.d.ts.map +1 -0
  67. package/dist/task-subtree-transfer/planner.d.ts +24 -0
  68. package/dist/task-subtree-transfer/planner.d.ts.map +1 -0
  69. package/dist/task-subtree-transfer/postgres.d.ts +19 -0
  70. package/dist/task-subtree-transfer/postgres.d.ts.map +1 -0
  71. package/dist/task-subtree-transfer/schema-sql.d.ts +5 -0
  72. package/dist/task-subtree-transfer/schema-sql.d.ts.map +1 -0
  73. package/dist/task-subtree-transfer/sqlite.d.ts +16 -0
  74. package/dist/task-subtree-transfer/sqlite.d.ts.map +1 -0
  75. package/dist/task-subtree-transfer/types.d.ts +148 -0
  76. package/dist/task-subtree-transfer/types.d.ts.map +1 -0
  77. package/dist/task-subtree-transfer.d.ts +2 -0
  78. package/dist/task-subtree-transfer.d.ts.map +1 -0
  79. package/dist/task-subtree-transfer.js +1869 -0
  80. package/dist/testing.d.ts +41 -7
  81. package/dist/testing.d.ts.map +1 -1
  82. package/dist/testing.js +44 -1
  83. package/dist/types/index.d.ts +24 -0
  84. package/dist/types/index.d.ts.map +1 -1
  85. package/package.json +8 -4
package/dist/mcp/index.js CHANGED
@@ -2888,6 +2888,20 @@ function ensureSchema(db) {
2888
2888
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
2889
2889
  ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
2890
2890
  ensureColumn("task_comments", "progress_pct", "INTEGER");
2891
+ ensureTable("plan_comments", `
2892
+ CREATE TABLE plan_comments (
2893
+ id TEXT PRIMARY KEY,
2894
+ plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
2895
+ agent_id TEXT,
2896
+ session_id TEXT,
2897
+ content TEXT NOT NULL,
2898
+ type TEXT DEFAULT 'comment' CHECK(type IN ('comment', 'progress', 'note')),
2899
+ progress_pct INTEGER CHECK(progress_pct IS NULL OR (progress_pct >= 0 AND progress_pct <= 100)),
2900
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2901
+ machine_id TEXT,
2902
+ synced_at TEXT
2903
+ )`);
2904
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_plan_comments_plan ON plan_comments(plan_id)");
2891
2905
  ensureColumn("projects", "machine_id", "TEXT");
2892
2906
  ensureColumn("projects", "synced_at", "TEXT");
2893
2907
  ensureColumn("tasks", "machine_id", "TEXT");
@@ -3884,6 +3898,7 @@ var init_machines = __esm(() => {
3884
3898
  "task_lists",
3885
3899
  "plans",
3886
3900
  "task_comments",
3901
+ "plan_comments",
3887
3902
  "sessions",
3888
3903
  "task_history",
3889
3904
  "webhooks",
@@ -4112,20 +4127,16 @@ var init_sync_utils = __esm(() => {
4112
4127
  // src/storage/config.ts
4113
4128
  var exports_config = {};
4114
4129
  __export(exports_config, {
4115
- parseStorageMode: () => parseStorageMode,
4116
4130
  parseStorageBackend: () => parseStorageBackend,
4117
4131
  loadTodosStorageConfig: () => loadTodosStorageConfig,
4118
4132
  loadStorageConfig: () => loadStorageConfig,
4119
4133
  isTodosShadowEnabled: () => isTodosShadowEnabled,
4120
- isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
4121
4134
  isTodosPostgresBackend: () => isTodosPostgresBackend,
4122
4135
  getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
4123
- getTodosStorageMode: () => getTodosStorageMode,
4124
4136
  getTodosStorageEnvName: () => getTodosStorageEnvName,
4125
4137
  getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
4126
4138
  getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
4127
4139
  getTodosStorageBackend: () => getTodosStorageBackend,
4128
- getStorageMode: () => getStorageMode,
4129
4140
  getStorageDatabaseUrl: () => getStorageDatabaseUrl,
4130
4141
  getStorageDatabaseEnv: () => getStorageDatabaseEnv,
4131
4142
  getCanonicalTodosRdsConfig: () => getCanonicalTodosRdsConfig,
@@ -4135,7 +4146,6 @@ __export(exports_config, {
4135
4146
  TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
4136
4147
  TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
4137
4148
  STORAGE_TABLES: () => STORAGE_TABLES,
4138
- REMOVED_STORAGE_MODE_ENV_KEYS: () => REMOVED_STORAGE_MODE_ENV_KEYS,
4139
4149
  CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
4140
4150
  CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
4141
4151
  CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
@@ -4190,9 +4200,6 @@ function loadStorageConfig(env = process.env) {
4190
4200
  function isTodosPostgresBackend(config) {
4191
4201
  return config.mode === "postgres";
4192
4202
  }
4193
- function isTodosRemoteStorageEnabled(config) {
4194
- return isTodosPostgresBackend(config);
4195
- }
4196
4203
  function assertTodosRemoteStorageConfig(config) {
4197
4204
  if (!isTodosPostgresBackend(config))
4198
4205
  return;
@@ -4208,32 +4215,15 @@ function parseStorageBackend(value) {
4208
4215
  return "sqlite";
4209
4216
  if (normalized === "postgres" || normalized === "postgresql")
4210
4217
  return "postgres";
4211
- if (["local", "remote", "cloud", "hybrid", "self_hosted"].includes(normalized)) {
4212
- 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.`);
4213
- }
4214
4218
  throw new Error(`Storage backend must be sqlite or postgres`);
4215
4219
  }
4216
- function parseStorageMode(value) {
4217
- return parseStorageBackend(value);
4218
- }
4219
4220
  function getTodosStorageBackend(env = process.env) {
4220
- for (const key of REMOVED_STORAGE_MODE_ENV_KEYS) {
4221
- if (Object.hasOwn(env, key) && env[key] !== undefined) {
4222
- 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.`);
4223
- }
4224
- }
4225
4221
  if (!getTodosStorageDatabaseUrl(env))
4226
4222
  return "sqlite";
4227
4223
  if (isTodosShadowEnabled(env))
4228
4224
  return "sqlite";
4229
4225
  return "postgres";
4230
4226
  }
4231
- function getTodosStorageMode(env = process.env) {
4232
- return getTodosStorageBackend(env);
4233
- }
4234
- function getStorageMode(env = process.env) {
4235
- return getTodosStorageBackend(env);
4236
- }
4237
4227
  function isTodosShadowEnabled(env = process.env) {
4238
4228
  return parseBoolean(readStorageEnv(env, "shadow").value, false);
4239
4229
  }
@@ -4300,7 +4290,7 @@ function parsePositiveInteger(value, fallback) {
4300
4290
  }
4301
4291
  return parsed;
4302
4292
  }
4303
- 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";
4293
+ 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";
4304
4294
  var init_config = __esm(() => {
4305
4295
  TODOS_STORAGE_TABLES = [
4306
4296
  "todos_sync_records",
@@ -4339,12 +4329,6 @@ var init_config = __esm(() => {
4339
4329
  syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
4340
4330
  syncDryRun: "TODOS_SYNC_DRY_RUN"
4341
4331
  };
4342
- REMOVED_STORAGE_MODE_ENV_KEYS = [
4343
- "HASNA_TODOS_STORAGE_MODE",
4344
- "HASNA_TODOS_MODE",
4345
- "TODOS_STORAGE_MODE",
4346
- "TODOS_MODE"
4347
- ];
4348
4332
  });
4349
4333
 
4350
4334
  // src/storage/shadow-outbox-schema.ts
@@ -11747,22 +11731,31 @@ function updateTaskList(id, input, db) {
11747
11731
  throw new TaskListNotFoundError(id);
11748
11732
  const sets = ["updated_at = ?"];
11749
11733
  const params = [now()];
11750
- if (input.slug !== undefined) {
11751
- const slug = slugify(input.slug);
11752
- if (!slug)
11753
- throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
11754
- 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);
11734
+ const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? existing.project_id : input.project_id;
11735
+ const scopeChanged = projectId !== existing.project_id;
11736
+ const effectiveSlug = input.slug !== undefined ? slugify(input.slug) : existing.slug;
11737
+ if (input.slug !== undefined && !effectiveSlug) {
11738
+ throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
11739
+ }
11740
+ if (scopeChanged || input.slug !== undefined && effectiveSlug !== existing.slug) {
11741
+ 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);
11755
11742
  if (duplicate) {
11756
- throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
11743
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
11757
11744
  }
11758
- if (slug !== existing.slug) {
11745
+ if (scopeChanged || effectiveSlug !== existing.slug) {
11759
11746
  releaseCanonicalSlugClaims("task_list", id, d);
11760
- if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(existing.project_id), slug, id, d)) {
11761
- throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
11747
+ if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(projectId), effectiveSlug, id, d)) {
11748
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
11762
11749
  }
11763
11750
  }
11751
+ }
11752
+ if (input.slug !== undefined) {
11764
11753
  sets.push("slug = ?");
11765
- params.push(slug);
11754
+ params.push(effectiveSlug);
11755
+ }
11756
+ if (projectId !== existing.project_id) {
11757
+ sets.push("project_id = ?");
11758
+ params.push(projectId);
11766
11759
  }
11767
11760
  if (input.name !== undefined) {
11768
11761
  sets.push("name = ?");
@@ -13959,8 +13952,8 @@ function failTask(id, agentId, reason, options, db) {
13959
13952
  const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
13960
13953
  const timestamp2 = now();
13961
13954
  const failTx = d.transaction(() => {
13962
- const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
13963
- WHERE id = ? AND version = ?`, [safeReason, JSON.stringify(safeMeta), timestamp2, id, task.version]);
13955
+ 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 = ?
13956
+ WHERE id = ? AND version = ?`, [safeReason, timestamp2, JSON.stringify(safeMeta), timestamp2, id, task.version]);
13964
13957
  if (res.changes === 0) {
13965
13958
  const current = getTask(id, d);
13966
13959
  throw new VersionConflictError(id, task.version, current?.version ?? -1);
@@ -13972,6 +13965,7 @@ function failTask(id, agentId, reason, options, db) {
13972
13965
  status: "failed",
13973
13966
  locked_by: null,
13974
13967
  locked_at: null,
13968
+ completed_at: task.completed_at ?? timestamp2,
13975
13969
  reason: safeReason,
13976
13970
  metadata: safeMeta,
13977
13971
  version: task.version + 1,
@@ -14620,13 +14614,20 @@ function updateTaskStored(id, input, db) {
14620
14614
  if (isTerminalStatus(input.status)) {
14621
14615
  sets.push("locked_by = NULL");
14622
14616
  sets.push("locked_at = NULL");
14623
- }
14624
- if (input.status === "completed") {
14625
- sets.push("completed_at = ?");
14626
- params.push(completionTimestamp);
14617
+ if (input.status === "completed") {
14618
+ sets.push("completed_at = ?");
14619
+ params.push(completionTimestamp);
14620
+ } else {
14621
+ sets.push("completed_at = COALESCE(completed_at, ?)");
14622
+ params.push(completionTimestamp);
14623
+ }
14627
14624
  } else if (task.status === "completed" && input.completed_at === undefined) {
14628
14625
  sets.push("completed_at = NULL");
14629
14626
  }
14627
+ if (input.status === "in_progress") {
14628
+ sets.push("started_at = COALESCE(started_at, ?)");
14629
+ params.push(timestamp2);
14630
+ }
14630
14631
  }
14631
14632
  if (input.priority !== undefined) {
14632
14633
  sets.push("priority = ?");
@@ -14764,7 +14765,6 @@ function updateTaskStored(id, input, db) {
14764
14765
  if (input.approved_by !== undefined)
14765
14766
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
14766
14767
  const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
14767
- const completedNow = input.status === "completed";
14768
14768
  const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
14769
14769
  const updatedTask = {
14770
14770
  ...task,
@@ -14775,7 +14775,8 @@ function updateTaskStored(id, input, db) {
14775
14775
  updated_at: timestamp2,
14776
14776
  locked_by: terminalNow ? null : task.locked_by,
14777
14777
  locked_at: terminalNow ? null : task.locked_at,
14778
- completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
14778
+ completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : terminalNow ? task.completed_at ?? completionTimestamp : reopened ? null : task.completed_at,
14779
+ started_at: input.status === "in_progress" ? task.started_at ?? timestamp2 : task.started_at,
14779
14780
  sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
14780
14781
  actual_minutes: input.actual_minutes ?? task.actual_minutes,
14781
14782
  confidence: input.confidence !== undefined ? input.confidence : task.confidence,
@@ -14836,6 +14837,9 @@ function deleteTask(id, db) {
14836
14837
  version: row.version
14837
14838
  }, d);
14838
14839
  const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
14840
+ if (result.changes > 0) {
14841
+ d.run("DELETE FROM task_dependencies WHERE task_id = ? OR depends_on = ?", [id, id]);
14842
+ }
14839
14843
  return result.changes > 0;
14840
14844
  }
14841
14845
  var init_task_crud = __esm(() => {
@@ -15100,8 +15104,10 @@ function bulkDeleteTasks(taskIds, force = false, db) {
15100
15104
  continue;
15101
15105
  }
15102
15106
  const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
15103
- if (result.changes > 0)
15107
+ if (result.changes > 0) {
15108
+ d.run("DELETE FROM task_dependencies WHERE task_id = ? OR depends_on = ?", [id, id]);
15104
15109
  deleted++;
15110
+ }
15105
15111
  } catch (e) {
15106
15112
  failed.push({ id, error: e instanceof Error ? e.message : String(e) });
15107
15113
  }
@@ -21187,20 +21193,25 @@ var init_page_validation = __esm(() => {
21187
21193
  import { resolveStorageClient } from "@hasna/contracts/client/storage";
21188
21194
  import { randomUUID as randomUUID3 } from "crypto";
21189
21195
  import { resolve as resolvePath } from "path";
21190
- function assertNoLegacyStorageMode(env = process.env) {
21191
- for (const key of LEGACY_STORAGE_MODE_KEYS) {
21192
- if (Object.hasOwn(env, key) && env[key] !== undefined) {
21193
- throw new Error(`REMOTE_STORAGE_MODE_REMOVED: ${key} 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.`);
21194
- }
21195
- }
21196
+ function emitTodosLocalFallbackNotice(env) {
21197
+ if (todosLocalFallbackNoticeEmitted)
21198
+ return;
21199
+ todosLocalFallbackNoticeEmitted = true;
21200
+ const notice = {
21201
+ event: "todos-local-fallback",
21202
+ transport: "sqlite",
21203
+ source: "default",
21204
+ apiUrlPresent: Boolean(env.HASNA_TODOS_API_URL?.trim()),
21205
+ apiKeyPresent: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
21206
+ 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."
21207
+ };
21208
+ console.error(JSON.stringify(notice));
21196
21209
  }
21197
- function resolveTodosCliStorageMode(env = process.env) {
21198
- assertNoLegacyStorageMode(env);
21210
+ function resolveTodosCliTransport(env = process.env) {
21199
21211
  const urlValue = env.HASNA_TODOS_API_URL?.trim();
21200
21212
  const keyValue = env.HASNA_TODOS_API_KEY?.trim();
21201
21213
  if (urlValue && keyValue) {
21202
21214
  return {
21203
- mode: "http",
21204
21215
  transport: "http",
21205
21216
  selected: true,
21206
21217
  source: "HASNA_TODOS_API_URL+HASNA_TODOS_API_KEY"
@@ -21212,15 +21223,15 @@ function resolveTodosCliStorageMode(env = process.env) {
21212
21223
  if (keyValue) {
21213
21224
  throw new Error("REMOTE_API_URL_MISSING: remote Todos storage requires HASNA_TODOS_API_URL; local SQLite fallback is disabled");
21214
21225
  }
21226
+ emitTodosLocalFallbackNotice(env);
21215
21227
  return {
21216
- mode: "sqlite",
21217
21228
  transport: "sqlite",
21218
21229
  selected: false,
21219
21230
  source: "default"
21220
21231
  };
21221
21232
  }
21222
21233
  function requestedTransport(env) {
21223
- return resolveTodosCliStorageMode(env).transport;
21234
+ return resolveTodosCliTransport(env).transport;
21224
21235
  }
21225
21236
  function normalizeRemoteAuthorityUrl(value) {
21226
21237
  const trimmed = value?.trim();
@@ -21254,13 +21265,13 @@ function normalizeRemoteAuthorityUrl(value) {
21254
21265
  function getTodosRemoteAuthorityConfigStatus(env = process.env) {
21255
21266
  let resolution;
21256
21267
  try {
21257
- resolution = resolveTodosCliStorageMode(env);
21268
+ resolution = resolveTodosCliTransport(env);
21258
21269
  } catch (error) {
21259
21270
  const issue = error instanceof Error ? error.message : String(error);
21260
21271
  return {
21261
21272
  selected: true,
21262
21273
  ok: false,
21263
- mode: "invalid",
21274
+ transport: "invalid",
21264
21275
  api_url_configured: Boolean(env.HASNA_TODOS_API_URL?.trim()),
21265
21276
  api_key_configured: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
21266
21277
  v1_base_url: null,
@@ -21268,12 +21279,12 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
21268
21279
  local_fallback: false
21269
21280
  };
21270
21281
  }
21271
- const { mode, selected } = resolution;
21282
+ const { transport, selected } = resolution;
21272
21283
  if (!selected) {
21273
21284
  return {
21274
21285
  selected: false,
21275
21286
  ok: true,
21276
- mode,
21287
+ transport,
21277
21288
  api_url_configured: false,
21278
21289
  api_key_configured: false,
21279
21290
  v1_base_url: null,
@@ -21298,7 +21309,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
21298
21309
  return {
21299
21310
  selected: true,
21300
21311
  ok: issues.length === 0,
21301
- mode,
21312
+ transport,
21302
21313
  api_url_configured: apiUrl !== null,
21303
21314
  api_key_configured: apiKeyConfigured,
21304
21315
  v1_base_url: apiUrl ? `${apiUrl}/v1` : null,
@@ -21342,7 +21353,8 @@ function classifyRemoteRequestError(baseUrl, route, error) {
21342
21353
  }
21343
21354
  const message = error instanceof Error ? error.message : String(error);
21344
21355
  if (error instanceof Error && error.name === "AbortError" || /abort|timed?\s*out/i.test(message)) {
21345
- throw new Error(`REMOTE_API_TIMEOUT: configured Todos authority ${baseUrl} timed out for ${route}; local SQLite fallback is disabled`, { cause: error });
21356
+ 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";
21357
+ throw new Error(`REMOTE_API_TIMEOUT: configured Todos authority ${baseUrl} timed out for ${route}${unboundedNote}`, { cause: error });
21346
21358
  }
21347
21359
  if (status === undefined) {
21348
21360
  throw new Error(`REMOTE_API_UNREACHABLE: configured Todos authority ${baseUrl} could not be reached for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
@@ -21429,11 +21441,10 @@ function getTodosCloudClient(env = process.env, requestTimeoutMs = REMOTE_REQUES
21429
21441
  fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
21430
21442
  timeoutMs: requestTimeoutMs
21431
21443
  });
21432
- if (resolved.transport === "cloud-http")
21433
- return protectRemoteClient(resolved.client, requestTimeoutMs);
21434
21444
  const transportName = resolved.transport;
21435
- if (transportName === "http")
21445
+ if (transportName === "cloud-http" || transportName === "http") {
21436
21446
  return protectRemoteClient(resolved.client, requestTimeoutMs);
21447
+ }
21437
21448
  return null;
21438
21449
  }
21439
21450
  function unwrapTask(raw) {
@@ -21905,7 +21916,7 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
21905
21916
  return input.toLowerCase();
21906
21917
  return (await cloudResolveTaskList(client, ref, projectId)).id;
21907
21918
  }
21908
- var UUID_RE, completionCapabilityCache, retryCapabilityCache, taskCreatorCapabilityCache, gitRefCapabilityCache, remoteCommandCapabilityCache, LEGACY_STORAGE_MODE_KEYS, REMOTE_REQUEST_TIMEOUT_MS = 1e4, PRIORITY_RANK, listTagsCapabilityCache;
21919
+ var UUID_RE, completionCapabilityCache, retryCapabilityCache, taskCreatorCapabilityCache, gitRefCapabilityCache, remoteCommandCapabilityCache, todosLocalFallbackNoticeEmitted = false, REMOTE_REQUEST_TIMEOUT_MS = 1e4, PRIORITY_RANK, listTagsCapabilityCache;
21909
21920
  var init_cloud_router = __esm(() => {
21910
21921
  init_types();
21911
21922
  init_redaction();
@@ -21919,12 +21930,6 @@ var init_cloud_router = __esm(() => {
21919
21930
  taskCreatorCapabilityCache = new Map;
21920
21931
  gitRefCapabilityCache = new Map;
21921
21932
  remoteCommandCapabilityCache = new Map;
21922
- LEGACY_STORAGE_MODE_KEYS = [
21923
- "HASNA_TODOS_STORAGE_MODE",
21924
- "HASNA_TODOS_MODE",
21925
- "TODOS_STORAGE_MODE",
21926
- "TODOS_MODE"
21927
- ];
21928
21933
  PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
21929
21934
  listTagsCapabilityCache = new Map;
21930
21935
  });
@@ -36042,7 +36047,7 @@ var package_default;
36042
36047
  var init_package = __esm(() => {
36043
36048
  package_default = {
36044
36049
  name: "@hasna/todos",
36045
- version: "0.15.35",
36050
+ version: "0.15.41",
36046
36051
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
36047
36052
  type: "module",
36048
36053
  main: "dist/index.js",
@@ -36088,6 +36093,10 @@ var init_package = __esm(() => {
36088
36093
  "./task-manifest": {
36089
36094
  types: "./dist/task-manifest.d.ts",
36090
36095
  import: "./dist/task-manifest.js"
36096
+ },
36097
+ "./task-subtree-transfer": {
36098
+ types: "./dist/task-subtree-transfer.d.ts",
36099
+ import: "./dist/task-subtree-transfer.js"
36091
36100
  }
36092
36101
  },
36093
36102
  workspaces: [
@@ -36101,8 +36110,8 @@ var init_package = __esm(() => {
36101
36110
  "README.md"
36102
36111
  ],
36103
36112
  scripts: {
36104
- 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",
36105
- "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/*'",
36113
+ 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",
36114
+ "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/*'",
36106
36115
  migrate: "bun run src/server/index.ts migrate",
36107
36116
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
36108
36117
  "generate:sdk": "bun run scripts/generate-sdk.ts",
@@ -36156,7 +36165,7 @@ var init_package = __esm(() => {
36156
36165
  author: "Andrei Hasna <andrei@hasna.com>",
36157
36166
  license: "Apache-2.0",
36158
36167
  dependencies: {
36159
- "@hasna/contracts": "0.5.2",
36168
+ "@hasna/contracts": "0.13.3",
36160
36169
  "@hasna/events": "^0.1.11",
36161
36170
  "@modelcontextprotocol/sdk": "^1.12.1",
36162
36171
  chalk: "^5.4.1",
@@ -45433,6 +45442,75 @@ var init_cloud_client = __esm(() => {
45433
45442
  init_config();
45434
45443
  });
45435
45444
 
45445
+ // src/db/plan-comments.ts
45446
+ function addPlanComment(input, db) {
45447
+ const d = db || getDatabase();
45448
+ if (!getPlan(input.plan_id, d)) {
45449
+ throw new PlanNotFoundError(input.plan_id);
45450
+ }
45451
+ const id = uuid();
45452
+ const timestamp4 = now();
45453
+ d.run(`INSERT INTO plan_comments (id, plan_id, agent_id, session_id, content, type, progress_pct, created_at)
45454
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
45455
+ id,
45456
+ input.plan_id,
45457
+ input.agent_id || null,
45458
+ input.session_id || null,
45459
+ sanitizePreWriteText(input.content, "comment.content"),
45460
+ input.type || "comment",
45461
+ input.progress_pct ?? null,
45462
+ timestamp4
45463
+ ]);
45464
+ return getPlanComment(id, d);
45465
+ }
45466
+ function getPlanComment(id, db) {
45467
+ const d = db || getDatabase();
45468
+ return d.query("SELECT * FROM plan_comments WHERE id = ?").get(id);
45469
+ }
45470
+ function listPlanComments(planId, db) {
45471
+ const d = db || getDatabase();
45472
+ return d.query("SELECT * FROM plan_comments WHERE plan_id = ? ORDER BY created_at, rowid").all(planId);
45473
+ }
45474
+ var init_plan_comments = __esm(() => {
45475
+ init_types();
45476
+ init_database();
45477
+ init_plans();
45478
+ init_prewrite_secrets();
45479
+ });
45480
+
45481
+ // src/lib/comment-cursor.ts
45482
+ function encodeCommentCursor(comment) {
45483
+ return Buffer.from(JSON.stringify({ created_at: comment.created_at, id: comment.id }), "utf8").toString("base64url");
45484
+ }
45485
+ function decodeCommentCursor(value) {
45486
+ if (value.length > MAX_COMMENT_CURSOR_LENGTH)
45487
+ throw new Error("invalid comment cursor");
45488
+ let parsed;
45489
+ try {
45490
+ parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
45491
+ } catch {
45492
+ throw new Error("invalid comment cursor");
45493
+ }
45494
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
45495
+ throw new Error("invalid comment cursor");
45496
+ const cursor = parsed;
45497
+ if (typeof cursor["created_at"] !== "string" || cursor["created_at"].length > 64 || !Number.isFinite(Date.parse(cursor["created_at"])) || typeof cursor["id"] !== "string" || !cursor["id"] || cursor["id"].length > 256) {
45498
+ throw new Error("invalid comment cursor");
45499
+ }
45500
+ return { created_at: cursor["created_at"], id: cursor["id"] };
45501
+ }
45502
+ function isStrictlyOlder(comment, before) {
45503
+ return comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id;
45504
+ }
45505
+ function compareCommentKeyset(left, right) {
45506
+ if (isStrictlyOlder(left, right))
45507
+ return -1;
45508
+ if (isStrictlyOlder(right, left))
45509
+ return 1;
45510
+ return 0;
45511
+ }
45512
+ var MAX_COMMENT_CURSOR_LENGTH = 1024;
45513
+
45436
45514
  // src/db/plan-project-links.ts
45437
45515
  function getPlanProjectLinkReceipt(receiptId, db) {
45438
45516
  const d = db || getDatabase();
@@ -46239,7 +46317,22 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
46239
46317
  list: (projectId) => listPlans(projectId, database()),
46240
46318
  update: (id, input) => updatePlan(id, input, database()),
46241
46319
  completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
46242
- delete: (id) => deletePlan(id, database())
46320
+ delete: (id) => deletePlan(id, database()),
46321
+ addComment: (input) => addPlanComment(input, database()),
46322
+ getComments: (planId) => listPlanComments(planId, database()),
46323
+ getCommentsPage: (planId, options2) => {
46324
+ if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
46325
+ throw new Error("Plan comment limit must be an integer between 1 and 1001");
46326
+ }
46327
+ let comments = listPlanComments(planId, database());
46328
+ comments = comments.sort(compareCommentKeyset);
46329
+ if (options2?.before) {
46330
+ const before = options2.before;
46331
+ comments = comments.filter((comment) => isStrictlyOlder(comment, before));
46332
+ }
46333
+ const limit = options2?.limit ?? 100;
46334
+ return comments.slice(-limit);
46335
+ }
46243
46336
  },
46244
46337
  planProjectLinks: {
46245
46338
  apply: (input) => applyPlanProjectLinkSqlite(input, database()),
@@ -46280,10 +46373,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
46280
46373
  throw new Error("Comment limit must be an integer between 1 and 1001");
46281
46374
  }
46282
46375
  let comments = listComments(taskId, database());
46283
- comments = comments.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
46376
+ comments = comments.sort(compareCommentKeyset);
46284
46377
  if (options2?.before) {
46285
46378
  const before = options2.before;
46286
- comments = comments.filter((comment) => comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id);
46379
+ comments = comments.filter((comment) => isStrictlyOlder(comment, before));
46287
46380
  }
46288
46381
  if (options2?.limit !== undefined)
46289
46382
  comments = comments.slice(-options2.limit);
@@ -46314,6 +46407,7 @@ var init_local_sqlite = __esm(() => {
46314
46407
  init_tasks();
46315
46408
  init_projects();
46316
46409
  init_plans();
46410
+ init_plan_comments();
46317
46411
  init_agents();
46318
46412
  init_task_lists();
46319
46413
  init_templates();
@@ -47226,7 +47320,26 @@ function createPostgresTodosStorageAdapter(options) {
47226
47320
  list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
47227
47321
  update: (id, input) => updatePlan2(id, input, store),
47228
47322
  completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
47229
- delete: (id, context) => store.deletePlan(id, context)
47323
+ delete: (id, context) => store.deletePlan(id, context),
47324
+ addComment: (input, context) => addPlanComment2(input, store, context),
47325
+ getComments: async (planId) => {
47326
+ const pages = [];
47327
+ let before;
47328
+ while (true) {
47329
+ const page = await store.listPlanComments(planId, { limit: 1000, ...before ? { before } : {} });
47330
+ if (page.length === 0)
47331
+ break;
47332
+ pages.unshift(page);
47333
+ if (page.length < 1000)
47334
+ break;
47335
+ const oldest = page[0];
47336
+ before = { created_at: oldest.created_at, id: oldest.id };
47337
+ }
47338
+ return pages.flat().map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
47339
+ },
47340
+ getCommentsPage: async (planId, options2) => {
47341
+ return (await store.listPlanComments(planId, options2)).map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
47342
+ }
47230
47343
  },
47231
47344
  planProjectLinks: {
47232
47345
  apply: (input, context) => store.applyPlanProjectLink(input, context),
@@ -47444,6 +47557,27 @@ class PostgresJsonRecordStore {
47444
47557
  LIMIT $${params.length}`, params);
47445
47558
  return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
47446
47559
  }
47560
+ async listPlanComments(planId, options = {}) {
47561
+ await this.ensureSchema();
47562
+ const limit = options.limit ?? 100;
47563
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1001) {
47564
+ throw new Error("Postgres plan comment limit must be an integer between 1 and 1001");
47565
+ }
47566
+ const params = [this.service, planId];
47567
+ let cursorPredicate = "";
47568
+ if (options.before) {
47569
+ params.push(options.before.created_at, options.before.id);
47570
+ cursorPredicate = `AND (payload->>'created_at', object_id) < ($3, $4)`;
47571
+ }
47572
+ params.push(limit);
47573
+ const result = await this.options.client.query(`/* todos:list-plan-comments */ SELECT payload FROM ${this.tableName}
47574
+ WHERE service = $1 AND object_type = 'plan_comments' AND deleted_at IS NULL
47575
+ AND payload->>'plan_id' = $2
47576
+ ${cursorPredicate}
47577
+ ORDER BY payload->>'created_at' DESC, object_id DESC
47578
+ LIMIT $${params.length}`, params);
47579
+ return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
47580
+ }
47447
47581
  async listRecords(type) {
47448
47582
  await this.ensureSchema();
47449
47583
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
@@ -48605,6 +48739,15 @@ class PostgresJsonRecordStore {
48605
48739
  WHERE r.service = $1 AND r.object_type = 'tasks' AND r.deleted_at IS NULL
48606
48740
  AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
48607
48741
  RETURNING 1
48742
+ ), deleted_plan_comments AS (
48743
+ UPDATE ${this.tableName} r SET
48744
+ deleted_at = $3::timestamptz,
48745
+ updated_at = $3::timestamptz,
48746
+ source_machine_id = COALESCE($4, r.source_machine_id),
48747
+ version = COALESCE(r.version, 0) + 1
48748
+ WHERE r.service = $1 AND r.object_type = 'plan_comments' AND r.deleted_at IS NULL
48749
+ AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
48750
+ RETURNING 1
48608
48751
  ), deleted_plan AS (
48609
48752
  UPDATE ${this.tableName} r SET
48610
48753
  deleted_at = $3::timestamptz,
@@ -48780,7 +48923,8 @@ async function updateTask2(id, input, store, context) {
48780
48923
  task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
48781
48924
  parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
48782
48925
  created_by: existing.created_by,
48783
- completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
48926
+ 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,
48927
+ started_at: input.status === "in_progress" ? existing.started_at ?? new Date().toISOString() : existing.started_at
48784
48928
  };
48785
48929
  const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
48786
48930
  operation: "update",
@@ -48825,7 +48969,8 @@ async function failTask2(id, agentId, reason, options, store) {
48825
48969
  status: "failed",
48826
48970
  assigned_to: task2.assigned_to ?? agentId ?? null,
48827
48971
  reason: reason ?? task2.reason,
48828
- retry_after: options?.retry_after ?? task2.retry_after
48972
+ retry_after: options?.retry_after ?? task2.retry_after,
48973
+ completed_at: task2.completed_at ?? new Date().toISOString()
48829
48974
  }, store);
48830
48975
  if (!options?.retry)
48831
48976
  return { task: failed };
@@ -49270,16 +49415,22 @@ async function createTaskList2(input, store, context) {
49270
49415
  async function updateTaskList2(id, input, store) {
49271
49416
  const list = await requireRecord("task_lists", id, store);
49272
49417
  const patch = definedPatch(input);
49418
+ const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? list.project_id : input.project_id;
49273
49419
  if (input.slug !== undefined) {
49274
49420
  const slug = slugifyRaw(input.slug);
49275
49421
  if (!slug)
49276
49422
  throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
49277
- const duplicate = (await store.list("task_lists")).find((candidate) => candidate.id !== id && candidate.project_id === list.project_id && candidate.slug === slug);
49423
+ patch.slug = slug;
49424
+ }
49425
+ const effectiveSlug = patch.slug ?? list.slug;
49426
+ if (projectId !== list.project_id || effectiveSlug !== list.slug) {
49427
+ const duplicate = (await store.list("task_lists")).find((candidate) => candidate.id !== id && candidate.project_id === projectId && candidate.slug === effectiveSlug);
49278
49428
  if (duplicate) {
49279
- throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
49429
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
49280
49430
  }
49281
- patch.slug = slug;
49282
49431
  }
49432
+ if (projectId !== list.project_id)
49433
+ patch.project_id = projectId;
49283
49434
  return store.upsert("task_lists", {
49284
49435
  ...list,
49285
49436
  ...patch,
@@ -49372,6 +49523,22 @@ async function addComment2(input, store, context) {
49372
49523
  function redactComment2(comment) {
49373
49524
  return { ...comment, content: redactEvidenceText(comment.content) };
49374
49525
  }
49526
+ async function addPlanComment2(input, store, context) {
49527
+ const comment = {
49528
+ id: randomUUID4(),
49529
+ plan_id: input.plan_id,
49530
+ agent_id: input.agent_id ?? context?.agentId ?? null,
49531
+ session_id: input.session_id ?? context?.sessionId ?? null,
49532
+ content: redactEvidenceText(input.content),
49533
+ type: input.type ?? "comment",
49534
+ progress_pct: input.progress_pct ?? null,
49535
+ created_at: new Date().toISOString()
49536
+ };
49537
+ return store.upsert("plan_comments", comment, context);
49538
+ }
49539
+ function redactPlanComment(comment) {
49540
+ return { ...comment, content: redactEvidenceText(comment.content) };
49541
+ }
49375
49542
  async function exportSnapshot(store) {
49376
49543
  return {
49377
49544
  exportedAt: new Date().toISOString(),
@@ -54191,6 +54358,924 @@ var init_task_manifest = __esm(() => {
54191
54358
  init_types5();
54192
54359
  });
54193
54360
 
54361
+ // src/task-subtree-transfer/types.ts
54362
+ var TODOS_TASK_SUBTREE_TRANSFER_ROUTE = "todos.task-subtree-transfer.v1", TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION = 1, TodosTaskSubtreeTransferError;
54363
+ var init_types6 = __esm(() => {
54364
+ TodosTaskSubtreeTransferError = class TodosTaskSubtreeTransferError extends Error {
54365
+ code;
54366
+ details;
54367
+ constructor(code, message, details = {}) {
54368
+ super(message);
54369
+ this.code = code;
54370
+ this.details = details;
54371
+ this.name = "TodosTaskSubtreeTransferError";
54372
+ }
54373
+ };
54374
+ });
54375
+
54376
+ // src/task-subtree-transfer/canonical.ts
54377
+ function taskSubtreeTransferRequestDigest(input) {
54378
+ const { idempotency_key: _idempotencyKey, ...request } = input;
54379
+ return canonicalDigest(request);
54380
+ }
54381
+ function taskSubtreeTransferRollbackRequestDigest(input) {
54382
+ const { idempotency_key: _idempotencyKey, ...request } = input;
54383
+ return canonicalDigest(request);
54384
+ }
54385
+ function deriveTodosTaskSubtreeTransferIdempotencyKey(input) {
54386
+ return `tstk_${canonicalDigest({
54387
+ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
54388
+ ...input
54389
+ }).slice(0, 48)}`;
54390
+ }
54391
+ var init_canonical2 = __esm(() => {
54392
+ init_canonical();
54393
+ init_types6();
54394
+ });
54395
+
54396
+ // src/task-subtree-transfer/planner.ts
54397
+ function sortedTaskImage(task3) {
54398
+ return {
54399
+ task_id: task3.task_id,
54400
+ project_id: task3.project_id,
54401
+ parent_id: task3.parent_id,
54402
+ plan_id: task3.plan_id,
54403
+ task_list_id: task3.task_list_id,
54404
+ version: task3.version,
54405
+ updated_at: task3.updated_at
54406
+ };
54407
+ }
54408
+ function sourcePopulationDigest(tasks) {
54409
+ return canonicalDigest(tasks.map((task3) => ({
54410
+ ...sortedTaskImage(task3),
54411
+ archived_at: task3.archived_at ?? null
54412
+ })).sort((left, right) => left.task_id.localeCompare(right.task_id)));
54413
+ }
54414
+ function closure(tasks, rootTaskId) {
54415
+ const byId = new Map(tasks.map((task3) => [task3.task_id, task3]));
54416
+ if (!byId.has(rootTaskId)) {
54417
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Root task not found in the exact source project: ${rootTaskId}`, { root_task_id: rootTaskId });
54418
+ }
54419
+ const children = new Map;
54420
+ for (const task3 of tasks) {
54421
+ if (!task3.parent_id)
54422
+ continue;
54423
+ const siblings = children.get(task3.parent_id) ?? [];
54424
+ siblings.push(task3.task_id);
54425
+ children.set(task3.parent_id, siblings);
54426
+ }
54427
+ for (const ids2 of children.values())
54428
+ ids2.sort();
54429
+ const visiting = new Set;
54430
+ const visited = new Set;
54431
+ const result = [];
54432
+ const visit = (id) => {
54433
+ if (visiting.has(id)) {
54434
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE", "Task hierarchy contains a cycle in the requested descendant closure", { task_id: id });
54435
+ }
54436
+ if (visited.has(id))
54437
+ return;
54438
+ const task3 = byId.get(id);
54439
+ if (!task3) {
54440
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE", "Task hierarchy references a parent outside the exact source population", { task_id: id });
54441
+ }
54442
+ visiting.add(id);
54443
+ result.push(task3);
54444
+ for (const child of children.get(id) ?? [])
54445
+ visit(child);
54446
+ visiting.delete(id);
54447
+ visited.add(id);
54448
+ };
54449
+ visit(rootTaskId);
54450
+ return result.sort((left, right) => left.task_id.localeCompare(right.task_id));
54451
+ }
54452
+ function prepareTransfer(snapshot, input) {
54453
+ if (!snapshot.destination_project_found || !snapshot.destination_task_list_found || !snapshot.destination_parent_found) {
54454
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE", "Destination project, task list, or parent does not match the exact requested target");
54455
+ }
54456
+ const moved = closure(snapshot.source_tasks, input.root_task_id);
54457
+ const movedIds = new Set(moved.map((task3) => task3.task_id));
54458
+ if (input.destination_parent_id && movedIds.has(input.destination_parent_id)) {
54459
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE", "Destination parent cannot be inside the transferred subtree", { destination_parent_id: input.destination_parent_id });
54460
+ }
54461
+ const planIds = [...new Set(moved.map((task3) => task3.plan_id).filter((id) => Boolean(id)))].sort();
54462
+ const allPlanTasks = new Map;
54463
+ for (const task3 of snapshot.plan_tasks) {
54464
+ if (!task3.plan_id)
54465
+ continue;
54466
+ const rows = allPlanTasks.get(task3.plan_id) ?? [];
54467
+ rows.push(task3);
54468
+ allPlanTasks.set(task3.plan_id, rows);
54469
+ }
54470
+ const contained = [];
54471
+ const shared = [];
54472
+ const planById = new Map(snapshot.plans.map((plan) => [plan.plan_id, plan]));
54473
+ for (const planId of planIds) {
54474
+ const plan = planById.get(planId);
54475
+ if (!plan || plan.project_id !== input.source_project_id) {
54476
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "A moved task references a plan absent from the exact source project", { plan_id: planId });
54477
+ }
54478
+ const members = allPlanTasks.get(planId) ?? [];
54479
+ if (members.length === 0) {
54480
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "A moved task references a plan with no authoritative task membership", { plan_id: planId });
54481
+ }
54482
+ if (members.every((task3) => movedIds.has(task3.task_id)))
54483
+ contained.push(planId);
54484
+ else
54485
+ shared.push(planId);
54486
+ }
54487
+ const inspection = {
54488
+ source_project_id: input.source_project_id,
54489
+ destination_project_id: input.destination_project_id,
54490
+ destination_task_list_id: input.destination_task_list_id,
54491
+ root_task_id: input.root_task_id,
54492
+ destination_parent_id: input.destination_parent_id,
54493
+ expected_root_parent_id: moved.find((task3) => task3.task_id === input.root_task_id).parent_id,
54494
+ source_population_digest: sourcePopulationDigest(snapshot.source_tasks),
54495
+ expected_tasks: moved.map((task3) => ({ task_id: task3.task_id, version: task3.version })),
54496
+ contained_plan_ids: contained,
54497
+ shared_plan_ids: shared,
54498
+ complete: true
54499
+ };
54500
+ return {
54501
+ inspection,
54502
+ prior_tasks: moved.map(sortedTaskImage),
54503
+ prior_plans: contained.map((id) => planById.get(id)),
54504
+ task_plan_targets: new Map(moved.map((task3) => [task3.task_id, task3.plan_id]))
54505
+ };
54506
+ }
54507
+ function validateApplySnapshot(prepared, request, plans) {
54508
+ if (request.expected_root_parent_id !== prepared.inspection.expected_root_parent_id) {
54509
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Root parent changed since inspection");
54510
+ }
54511
+ if (request.source_population_digest !== prepared.inspection.source_population_digest) {
54512
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_POPULATION_DRIFT", "Complete source-project task population changed since inspection");
54513
+ }
54514
+ 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));
54515
+ if (JSON.stringify(expected) !== JSON.stringify(prepared.inspection.expected_tasks)) {
54516
+ const actualById = new Map(prepared.inspection.expected_tasks.map((task3) => [task3.task_id, task3.version]));
54517
+ const sameIds = expected.length === prepared.inspection.expected_tasks.length && expected.every((task3) => actualById.has(task3.task_id));
54518
+ 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");
54519
+ }
54520
+ const requestedSources = request.shared_plan_splits.map((mapping) => mapping.source_plan_id).sort();
54521
+ if (JSON.stringify(requestedSources) !== JSON.stringify(prepared.inspection.shared_plan_ids)) {
54522
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PARTIAL_PLAN", "Shared plans require one exact explicit destination-plan mapping each", {
54523
+ expected_shared_plan_ids: prepared.inspection.shared_plan_ids,
54524
+ received_shared_plan_ids: requestedSources
54525
+ });
54526
+ }
54527
+ const planById = new Map(plans.map((plan) => [plan.plan_id, plan]));
54528
+ const destinationIds = new Set;
54529
+ for (const mapping of request.shared_plan_splits) {
54530
+ if (destinationIds.has(mapping.destination_plan_id)) {
54531
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "Two shared source plans cannot collapse into one destination plan");
54532
+ }
54533
+ destinationIds.add(mapping.destination_plan_id);
54534
+ const destination = planById.get(mapping.destination_plan_id);
54535
+ if (!destination || destination.project_id !== request.destination_project_id || destination.task_list_id !== request.destination_task_list_id) {
54536
+ 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 });
54537
+ }
54538
+ for (const [taskId, planId] of prepared.task_plan_targets) {
54539
+ if (planId === mapping.source_plan_id) {
54540
+ prepared.task_plan_targets.set(taskId, mapping.destination_plan_id);
54541
+ }
54542
+ }
54543
+ }
54544
+ }
54545
+ var init_planner = __esm(() => {
54546
+ init_canonical2();
54547
+ init_types6();
54548
+ });
54549
+
54550
+ // src/task-subtree-transfer/schema-sql.ts
54551
+ function postgresTodosTaskSubtreeTransferSchemaSql() {
54552
+ return [
54553
+ `CREATE TABLE IF NOT EXISTS todos_task_subtree_transfer_receipts (
54554
+ receipt_id text PRIMARY KEY,
54555
+ tenant_id text NOT NULL,
54556
+ kind text NOT NULL CHECK(kind IN ('apply', 'rollback')),
54557
+ operation_id text NOT NULL,
54558
+ step_id text NOT NULL,
54559
+ idempotency_key text NOT NULL,
54560
+ request_digest text NOT NULL,
54561
+ precondition_digest text NOT NULL,
54562
+ result_digest text NOT NULL,
54563
+ apply_receipt_id text,
54564
+ request_json jsonb NOT NULL,
54565
+ result_json jsonb NOT NULL,
54566
+ created_at timestamptz NOT NULL,
54567
+ UNIQUE(tenant_id, kind, operation_id, step_id),
54568
+ UNIQUE(tenant_id, kind, idempotency_key)
54569
+ )`,
54570
+ `CREATE OR REPLACE FUNCTION reject_todos_task_subtree_transfer_receipt_mutation()
54571
+ RETURNS trigger LANGUAGE plpgsql AS $$
54572
+ BEGIN
54573
+ RAISE EXCEPTION 'todos task subtree transfer receipts are immutable';
54574
+ END $$`,
54575
+ `DROP TRIGGER IF EXISTS todos_task_subtree_transfer_receipts_immutable_update
54576
+ ON todos_task_subtree_transfer_receipts`,
54577
+ `CREATE TRIGGER todos_task_subtree_transfer_receipts_immutable_update
54578
+ BEFORE UPDATE OR DELETE ON todos_task_subtree_transfer_receipts
54579
+ FOR EACH ROW EXECUTE FUNCTION reject_todos_task_subtree_transfer_receipt_mutation()`
54580
+ ];
54581
+ }
54582
+
54583
+ // src/task-subtree-transfer/postgres.ts
54584
+ function safeIdentifier3(value, fallback) {
54585
+ const candidate = value ?? fallback;
54586
+ if (!/^[a-z_][a-z0-9_]*$/.test(candidate)) {
54587
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "PostgreSQL table name must be a safe identifier");
54588
+ }
54589
+ return candidate;
54590
+ }
54591
+ function timestamp5(value) {
54592
+ return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
54593
+ }
54594
+ function payloadOf(row) {
54595
+ return typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload;
54596
+ }
54597
+ function taskRecord(row) {
54598
+ const payload = payloadOf(row);
54599
+ return {
54600
+ task_id: String(payload.id ?? row.object_id),
54601
+ project_id: payload.project_id == null ? null : String(payload.project_id),
54602
+ parent_id: payload.parent_id == null ? null : String(payload.parent_id),
54603
+ plan_id: payload.plan_id == null ? null : String(payload.plan_id),
54604
+ task_list_id: payload.task_list_id == null ? null : String(payload.task_list_id),
54605
+ version: Number(payload.version ?? row.version ?? 1),
54606
+ updated_at: timestamp5(payload.updated_at ?? row.updated_at),
54607
+ archived_at: payload.archived_at == null ? null : String(payload.archived_at)
54608
+ };
54609
+ }
54610
+ function planRecord(row) {
54611
+ const payload = payloadOf(row);
54612
+ return {
54613
+ plan_id: String(payload.id ?? row.object_id),
54614
+ project_id: payload.project_id == null ? null : String(payload.project_id),
54615
+ task_list_id: payload.task_list_id == null ? null : String(payload.task_list_id),
54616
+ updated_at: timestamp5(payload.updated_at ?? row.updated_at)
54617
+ };
54618
+ }
54619
+
54620
+ class PostgresTodosTaskSubtreeTransferBackend {
54621
+ client;
54622
+ kind = "postgresql";
54623
+ service;
54624
+ tableName;
54625
+ tenantId;
54626
+ schemaReady = null;
54627
+ constructor(client, options = {}) {
54628
+ this.client = client;
54629
+ this.service = options.service ?? "todos";
54630
+ this.tableName = safeIdentifier3(options.tableName, DEFAULT_TODOS_POSTGRES_SYNC_TABLE);
54631
+ this.tenantId = options.tenantId ?? "default";
54632
+ }
54633
+ async ensureSchema() {
54634
+ this.schemaReady ??= (async () => {
54635
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName))
54636
+ await this.client.query(sql);
54637
+ for (const sql of postgresTodosTaskSubtreeTransferSchemaSql())
54638
+ await this.client.query(sql);
54639
+ })();
54640
+ await this.schemaReady;
54641
+ }
54642
+ async snapshot(client, input, forUpdate = false) {
54643
+ const lock = forUpdate ? " FOR UPDATE" : "";
54644
+ const source3 = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
54645
+ WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL
54646
+ AND payload->>'project_id' = $2 ORDER BY object_id${lock}`, [this.service, input.source_project_id]);
54647
+ const allTasks = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
54648
+ WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL ORDER BY object_id${lock}`, [this.service]);
54649
+ const plans = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
54650
+ WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL ORDER BY object_id${lock}`, [this.service]);
54651
+ const destinationProject = await client.query(`SELECT 1 FROM ${this.tableName}
54652
+ WHERE service = $1 AND object_type = 'projects' AND object_id = $2
54653
+ AND deleted_at IS NULL LIMIT 1${lock}`, [this.service, input.destination_project_id]);
54654
+ const destinationList = await client.query(`SELECT 1 FROM ${this.tableName}
54655
+ WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2
54656
+ AND deleted_at IS NULL AND payload->>'project_id' = $3 LIMIT 1`, [this.service, input.destination_task_list_id, input.destination_project_id]);
54657
+ const destinationParent = input.destination_parent_id === null ? true : (await client.query(`SELECT 1 FROM ${this.tableName}
54658
+ WHERE service = $1 AND object_type = 'tasks' AND object_id = $2
54659
+ 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;
54660
+ return {
54661
+ source_tasks: source3.rows.map(taskRecord),
54662
+ plan_tasks: allTasks.rows.map(taskRecord),
54663
+ plans: plans.rows.map(planRecord),
54664
+ destination_project_found: destinationProject.rows.length === 1,
54665
+ destination_task_list_found: destinationList.rows.length === 1,
54666
+ destination_parent_found: destinationParent
54667
+ };
54668
+ }
54669
+ async inspect(input) {
54670
+ await this.ensureSchema();
54671
+ return prepareTransfer(await this.snapshot(this.client, input), input).inspection;
54672
+ }
54673
+ async insertPayload(client, task3, update, now4) {
54674
+ const current = await client.query(`SELECT payload FROM ${this.tableName}
54675
+ WHERE service = $1 AND object_type = 'tasks' AND object_id = $2
54676
+ AND deleted_at IS NULL FOR UPDATE`, [this.service, task3.task_id]);
54677
+ const row = current.rows[0];
54678
+ if (!row)
54679
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Task not found: ${task3.task_id}`);
54680
+ const payload = payloadOf(row);
54681
+ if (Number(payload.version ?? 1) !== task3.version) {
54682
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Task changed during transfer", { task_id: task3.task_id });
54683
+ }
54684
+ const next = {
54685
+ ...payload,
54686
+ ...update,
54687
+ version: task3.version + 1,
54688
+ updated_at: now4
54689
+ };
54690
+ const result = await client.query(`UPDATE ${this.tableName} SET payload = $1::jsonb, updated_at = $2, version = $3
54691
+ WHERE service = $4 AND object_type = 'tasks' AND object_id = $5
54692
+ AND deleted_at IS NULL AND version = $6
54693
+ RETURNING object_id`, [canonicalJson(next), now4, task3.version + 1, this.service, task3.task_id, task3.version]);
54694
+ if (result.rows.length !== 1) {
54695
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Task CAS failed", { task_id: task3.task_id });
54696
+ }
54697
+ }
54698
+ async apply(input, options) {
54699
+ await this.ensureSchema();
54700
+ if (!this.client.transaction) {
54701
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE", "PostgreSQL subtree transfer requires an authoritative transaction-capable client");
54702
+ }
54703
+ return this.client.transaction(async (tx) => {
54704
+ await tx.query("SELECT pg_advisory_xact_lock(hashtextextended('todos:task-parent-integrity', 0))");
54705
+ const requestDigest = taskSubtreeTransferRequestDigest(input);
54706
+ const existing = await tx.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
54707
+ FROM todos_task_subtree_transfer_receipts
54708
+ WHERE tenant_id = $1 AND kind = 'apply'
54709
+ AND (idempotency_key = $2 OR (operation_id = $3 AND step_id = $4))
54710
+ ORDER BY created_at ASC LIMIT 1`, [this.tenantId, input.idempotency_key, input.operation_id, input.step_id]);
54711
+ if (existing.rows[0]) {
54712
+ const row = existing.rows[0];
54713
+ if (row.request_digest !== requestDigest || row.precondition_digest !== input.precondition_digest || row.idempotency_key !== input.idempotency_key) {
54714
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The operation was already accepted for a different request");
54715
+ }
54716
+ return { ...typeof row.result_json === "string" ? JSON.parse(row.result_json) : row.result_json, duplicate: true };
54717
+ }
54718
+ const snapshot = await this.snapshot(tx, input, true);
54719
+ const prepared = prepareTransfer(snapshot, input);
54720
+ validateApplySnapshot(prepared, input, snapshot.plans);
54721
+ const now4 = options.now?.() ?? new Date().toISOString();
54722
+ const priorImage = {
54723
+ tasks: prepared.prior_tasks,
54724
+ plans: prepared.prior_plans
54725
+ };
54726
+ for (const task3 of prepared.prior_tasks) {
54727
+ await this.insertPayload(tx, task3, {
54728
+ project_id: input.destination_project_id,
54729
+ task_list_id: input.destination_task_list_id,
54730
+ plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
54731
+ parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id
54732
+ }, now4);
54733
+ }
54734
+ if (options.faultInjector && await options.faultInjector("after_task_writes"))
54735
+ throw new Error("Injected task-subtree-transfer fault at after_task_writes");
54736
+ for (const plan of prepared.prior_plans) {
54737
+ const row = await tx.query(`SELECT payload FROM ${this.tableName}
54738
+ WHERE service = $1 AND object_type = 'plans' AND object_id = $2
54739
+ AND deleted_at IS NULL FOR UPDATE`, [this.service, plan.plan_id]);
54740
+ const current = row.rows[0] ? payloadOf(row.rows[0]) : null;
54741
+ if (!current || String(current.project_id ?? "") !== String(plan.project_id ?? "") || (current.task_list_id ?? null) !== plan.task_list_id || timestamp5(current.updated_at ?? row.rows[0]?.updated_at) !== plan.updated_at) {
54742
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Contained plan changed during transfer", { plan_id: plan.plan_id });
54743
+ }
54744
+ 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
54745
+ WHERE service = $4 AND object_type = 'plans' AND object_id = $5 AND deleted_at IS NULL
54746
+ RETURNING object_id`, [input.destination_project_id, input.destination_task_list_id, now4, this.service, plan.plan_id]);
54747
+ if (result2.rows.length !== 1) {
54748
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Contained plan update did not affect exactly one row", { plan_id: plan.plan_id });
54749
+ }
54750
+ }
54751
+ if (options.faultInjector && await options.faultInjector("after_plan_writes"))
54752
+ throw new Error("Injected task-subtree-transfer fault at after_plan_writes");
54753
+ const postImage = {
54754
+ tasks: prepared.prior_tasks.map((task3) => ({
54755
+ ...task3,
54756
+ project_id: input.destination_project_id,
54757
+ task_list_id: input.destination_task_list_id,
54758
+ plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
54759
+ parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id,
54760
+ version: task3.version + 1,
54761
+ updated_at: now4
54762
+ })),
54763
+ plans: prepared.prior_plans.map((plan) => ({ ...plan, project_id: input.destination_project_id, task_list_id: input.destination_task_list_id, updated_at: now4 }))
54764
+ };
54765
+ const resultDigest = canonicalDigest({ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE, request_digest: requestDigest, prior_image: priorImage, post_image: postImage });
54766
+ const receipt = {
54767
+ receipt_id: deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "apply", input.operation_id, input.step_id, input.idempotency_key, requestDigest),
54768
+ authority: "todos",
54769
+ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
54770
+ schema_version: 1,
54771
+ kind: "apply",
54772
+ operation_id: input.operation_id,
54773
+ step_id: input.step_id,
54774
+ idempotency_key: input.idempotency_key,
54775
+ request_digest: requestDigest,
54776
+ precondition_digest: input.precondition_digest,
54777
+ result_digest: resultDigest,
54778
+ apply_receipt_id: null,
54779
+ source_project_id: input.source_project_id,
54780
+ destination_project_id: input.destination_project_id,
54781
+ destination_task_list_id: input.destination_task_list_id,
54782
+ root_task_id: input.root_task_id,
54783
+ source_population_digest: input.source_population_digest,
54784
+ prior_image: priorImage,
54785
+ post_image: postImage,
54786
+ shared_plan_splits: input.shared_plan_splits,
54787
+ created_at: now4
54788
+ };
54789
+ 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 };
54790
+ await tx.query(`INSERT INTO todos_task_subtree_transfer_receipts (
54791
+ receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
54792
+ request_digest, precondition_digest, result_digest, apply_receipt_id,
54793
+ request_json, result_json, created_at
54794
+ ) 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]);
54795
+ if (options.faultInjector && await options.faultInjector("after_receipt_write"))
54796
+ throw new Error("Injected task-subtree-transfer fault at after_receipt_write");
54797
+ return result;
54798
+ });
54799
+ }
54800
+ async readExact(receiptId2) {
54801
+ await this.ensureSchema();
54802
+ 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]);
54803
+ if (!row.rows[0])
54804
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Transfer receipt not found: ${receiptId2}`);
54805
+ return { ...typeof row.rows[0].result_json === "string" ? JSON.parse(String(row.rows[0].result_json)) : row.rows[0].result_json, duplicate: false };
54806
+ }
54807
+ async rollback(input, options) {
54808
+ await this.ensureSchema();
54809
+ if (!this.client.transaction)
54810
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE", "PostgreSQL subtree rollback requires a transaction-capable client");
54811
+ return this.client.transaction(async (tx) => {
54812
+ await tx.query("SELECT pg_advisory_xact_lock(hashtextextended('todos:task-parent-integrity', 0))");
54813
+ const requestDigest = taskSubtreeTransferRollbackRequestDigest(input);
54814
+ const existing = await tx.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
54815
+ FROM todos_task_subtree_transfer_receipts
54816
+ WHERE tenant_id = $1 AND kind = 'rollback'
54817
+ AND (idempotency_key = $2 OR (operation_id = $3 AND step_id = $4))
54818
+ ORDER BY created_at ASC LIMIT 1`, [this.tenantId, input.idempotency_key, input.operation_id, input.step_id]);
54819
+ if (existing.rows[0]) {
54820
+ const row = existing.rows[0];
54821
+ if (row.request_digest !== requestDigest || row.precondition_digest !== input.precondition_digest || row.idempotency_key !== input.idempotency_key)
54822
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The rollback operation was already accepted for a different request");
54823
+ return { ...typeof row.result_json === "string" ? JSON.parse(String(row.result_json)) : row.result_json, duplicate: true };
54824
+ }
54825
+ 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]);
54826
+ if (!applyRow.rows[0])
54827
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Apply receipt not found: ${input.receipt_id}`);
54828
+ const applied = typeof applyRow.rows[0].result_json === "string" ? JSON.parse(String(applyRow.rows[0].result_json)) : applyRow.rows[0].result_json;
54829
+ if (applied.receipt.operation_id !== input.operation_id) {
54830
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback operation does not match the apply receipt");
54831
+ }
54832
+ 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 });
54833
+ if (input.precondition_digest !== expectedPrecondition)
54834
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_DIGEST_MISMATCH", "Rollback precondition digest does not match the exact apply receipt", { expected_precondition_digest: expectedPrecondition });
54835
+ const now4 = options.now?.() ?? new Date().toISOString();
54836
+ for (const task3 of applied.receipt.post_image.tasks) {
54837
+ 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]);
54838
+ if (!row.rows[0])
54839
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Transferred task disappeared", { task_id: task3.task_id });
54840
+ const payload = payloadOf(row.rows[0]);
54841
+ 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)
54842
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Transferred task changed after apply", { task_id: task3.task_id });
54843
+ }
54844
+ for (const task3 of applied.receipt.prior_image.tasks) {
54845
+ 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]);
54846
+ const payload = row.rows[0] ? payloadOf(row.rows[0]) : null;
54847
+ if (!payload)
54848
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Task disappeared during rollback", { task_id: task3.task_id });
54849
+ 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 };
54850
+ const result2 = await tx.query(`UPDATE ${this.tableName} SET payload = $1::jsonb, updated_at = $2, version = $3
54851
+ WHERE service = $4 AND object_type = 'tasks' AND object_id = $5 AND deleted_at IS NULL AND version = $6
54852
+ RETURNING object_id`, [canonicalJson(next), now4, task3.version + 2, this.service, task3.task_id, task3.version + 1]);
54853
+ if (result2.rows.length !== 1) {
54854
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Task rollback did not affect exactly one row", { task_id: task3.task_id });
54855
+ }
54856
+ }
54857
+ if (options.faultInjector && await options.faultInjector("after_rollback_task_writes"))
54858
+ throw new Error("Injected task-subtree-transfer fault at after_rollback_task_writes");
54859
+ for (const plan of applied.receipt.prior_image.plans) {
54860
+ const post = applied.receipt.post_image.plans.find((candidate) => candidate.plan_id === plan.plan_id);
54861
+ const result2 = await tx.query(`UPDATE ${this.tableName}
54862
+ SET payload = jsonb_set(
54863
+ jsonb_set(
54864
+ jsonb_set(payload, '{project_id}', to_jsonb($1::text)),
54865
+ '{task_list_id}',
54866
+ COALESCE(to_jsonb($2::text), 'null'::jsonb)
54867
+ ),
54868
+ '{updated_at}',
54869
+ to_jsonb($3::text)
54870
+ ), updated_at = $3
54871
+ WHERE service = $4 AND object_type = 'plans' AND object_id = $5 AND deleted_at IS NULL
54872
+ AND payload->>'project_id' = $6 AND COALESCE(payload->>'task_list_id', '') = COALESCE($7, '') AND payload->>'updated_at' = $8
54873
+ 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]);
54874
+ if (result2.rows.length !== 1) {
54875
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Contained plan rollback did not affect exactly one row", { plan_id: plan.plan_id });
54876
+ }
54877
+ }
54878
+ if (options.faultInjector && await options.faultInjector("after_rollback_plan_writes"))
54879
+ throw new Error("Injected task-subtree-transfer fault at after_rollback_plan_writes");
54880
+ const restored = {
54881
+ tasks: applied.receipt.prior_image.tasks.map((task3) => ({ ...task3, version: task3.version + 2, updated_at: now4 })),
54882
+ plans: applied.receipt.prior_image.plans.map((plan) => ({ ...plan, updated_at: now4 }))
54883
+ };
54884
+ 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 });
54885
+ 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 };
54886
+ const result = { duplicate: false, receipt, moved_task_ids: applied.moved_task_ids, moved_plan_ids: applied.moved_plan_ids, complete: true };
54887
+ await tx.query(`INSERT INTO todos_task_subtree_transfer_receipts (
54888
+ receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
54889
+ request_digest, precondition_digest, result_digest, apply_receipt_id,
54890
+ request_json, result_json, created_at
54891
+ ) 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]);
54892
+ if (options.faultInjector && await options.faultInjector("after_rollback_receipt_write"))
54893
+ throw new Error("Injected task-subtree-transfer fault at after_rollback_receipt_write");
54894
+ return result;
54895
+ });
54896
+ }
54897
+ }
54898
+ var init_postgres4 = __esm(() => {
54899
+ init_postgres_sync();
54900
+ init_canonical2();
54901
+ init_planner();
54902
+ init_types6();
54903
+ });
54904
+
54905
+ // src/task-subtree-transfer/sqlite.ts
54906
+ var sqliteTails2;
54907
+ var init_sqlite3 = __esm(() => {
54908
+ init_canonical2();
54909
+ init_planner();
54910
+ init_types6();
54911
+ sqliteTails2 = new WeakMap;
54912
+ });
54913
+
54914
+ // src/task-subtree-transfer/authority.ts
54915
+ function boundedString(value, field, max = 200) {
54916
+ if (typeof value !== "string" || value.length === 0 || value.length > max) {
54917
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field} must be a non-empty bounded string`);
54918
+ }
54919
+ return value;
54920
+ }
54921
+ function uuid3(value, field) {
54922
+ const parsed = boundedString(value, field, 80);
54923
+ if (!UUID.test(parsed)) {
54924
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field} must be a UUID`);
54925
+ }
54926
+ return parsed;
54927
+ }
54928
+ function digest2(value, field) {
54929
+ const parsed = boundedString(value, field, 64);
54930
+ if (!DIGEST.test(parsed)) {
54931
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field} must be a SHA-256 digest`);
54932
+ }
54933
+ return parsed.toLowerCase();
54934
+ }
54935
+ function nullableUuid(value, field) {
54936
+ if (value === null)
54937
+ return null;
54938
+ return uuid3(value, field);
54939
+ }
54940
+ function strictObject(input, fields) {
54941
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
54942
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Request must be a JSON object");
54943
+ }
54944
+ const object = input;
54945
+ const allowed = new Set(fields);
54946
+ for (const key2 of Object.keys(object)) {
54947
+ if (!allowed.has(key2)) {
54948
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `Unknown field: ${key2}`);
54949
+ }
54950
+ }
54951
+ return object;
54952
+ }
54953
+ function parseTodosTaskSubtreeTransferInspect(input) {
54954
+ const value = strictObject(input, INSPECT_FIELDS);
54955
+ const request = {
54956
+ source_project_id: uuid3(value.source_project_id, "source_project_id"),
54957
+ destination_project_id: uuid3(value.destination_project_id, "destination_project_id"),
54958
+ destination_task_list_id: uuid3(value.destination_task_list_id, "destination_task_list_id"),
54959
+ root_task_id: uuid3(value.root_task_id, "root_task_id"),
54960
+ destination_parent_id: nullableUuid(value.destination_parent_id, "destination_parent_id")
54961
+ };
54962
+ if (request.source_project_id === request.destination_project_id) {
54963
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "source_project_id and destination_project_id must identify different Projects");
54964
+ }
54965
+ return request;
54966
+ }
54967
+ function parseTodosTaskSubtreeTransferApply(input) {
54968
+ const value = strictObject(input, APPLY_FIELDS);
54969
+ if (value.version !== 1) {
54970
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "version must be 1");
54971
+ }
54972
+ if (!Array.isArray(value.expected_tasks) || value.expected_tasks.length === 0) {
54973
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "expected_tasks must be a non-empty array");
54974
+ }
54975
+ const expected_tasks = value.expected_tasks.map((item, index) => {
54976
+ const row = strictObject(item, ["task_id", "version"]);
54977
+ const version = row.version;
54978
+ if (!Number.isSafeInteger(version) || Number(version) < 1) {
54979
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `expected_tasks.${index}.version must be a positive integer`);
54980
+ }
54981
+ return { task_id: uuid3(row.task_id, `expected_tasks.${index}.task_id`), version: Number(version) };
54982
+ });
54983
+ const ids2 = new Set;
54984
+ for (const row of expected_tasks) {
54985
+ if (ids2.has(row.task_id)) {
54986
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "expected_tasks must not contain duplicate task IDs");
54987
+ }
54988
+ ids2.add(row.task_id);
54989
+ }
54990
+ if (!Array.isArray(value.shared_plan_splits)) {
54991
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "shared_plan_splits must be an array");
54992
+ }
54993
+ const shared_plan_splits = value.shared_plan_splits.map((item, index) => {
54994
+ const row = strictObject(item, ["source_plan_id", "destination_plan_id"]);
54995
+ return {
54996
+ source_plan_id: uuid3(row.source_plan_id, `shared_plan_splits.${index}.source_plan_id`),
54997
+ destination_plan_id: uuid3(row.destination_plan_id, `shared_plan_splits.${index}.destination_plan_id`)
54998
+ };
54999
+ });
55000
+ return {
55001
+ ...parseTodosTaskSubtreeTransferInspect({
55002
+ source_project_id: value.source_project_id,
55003
+ destination_project_id: value.destination_project_id,
55004
+ destination_task_list_id: value.destination_task_list_id,
55005
+ root_task_id: value.root_task_id,
55006
+ destination_parent_id: value.destination_parent_id
55007
+ }),
55008
+ version: 1,
55009
+ operation_id: boundedString(value.operation_id, "operation_id"),
55010
+ step_id: boundedString(value.step_id, "step_id"),
55011
+ idempotency_key: boundedString(value.idempotency_key, "idempotency_key", 240),
55012
+ precondition_digest: digest2(value.precondition_digest, "precondition_digest"),
55013
+ expected_root_parent_id: nullableUuid(value.expected_root_parent_id, "expected_root_parent_id"),
55014
+ source_population_digest: digest2(value.source_population_digest, "source_population_digest"),
55015
+ expected_tasks,
55016
+ shared_plan_splits
55017
+ };
55018
+ }
55019
+ function parseTodosTaskSubtreeTransferRollback(input) {
55020
+ const value = strictObject(input, [
55021
+ "receipt_id",
55022
+ "operation_id",
55023
+ "step_id",
55024
+ "idempotency_key",
55025
+ "precondition_digest"
55026
+ ]);
55027
+ return {
55028
+ receipt_id: uuid3(value.receipt_id, "receipt_id"),
55029
+ operation_id: boundedString(value.operation_id, "operation_id"),
55030
+ step_id: boundedString(value.step_id, "step_id"),
55031
+ idempotency_key: boundedString(value.idempotency_key, "idempotency_key", 240),
55032
+ precondition_digest: digest2(value.precondition_digest, "precondition_digest")
55033
+ };
55034
+ }
55035
+ function deriveTodosTaskSubtreeTransferApplyPreconditionDigest(input) {
55036
+ const {
55037
+ source_project_id,
55038
+ destination_project_id,
55039
+ destination_task_list_id,
55040
+ root_task_id,
55041
+ expected_root_parent_id,
55042
+ destination_parent_id,
55043
+ source_population_digest,
55044
+ expected_tasks,
55045
+ shared_plan_splits
55046
+ } = input;
55047
+ return canonicalDigest({
55048
+ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
55049
+ direction: "apply",
55050
+ source_project_id,
55051
+ destination_project_id,
55052
+ destination_task_list_id,
55053
+ root_task_id,
55054
+ expected_root_parent_id,
55055
+ destination_parent_id,
55056
+ source_population_digest,
55057
+ expected_tasks,
55058
+ shared_plan_splits
55059
+ });
55060
+ }
55061
+ function resolveTenantId2(value) {
55062
+ const tenant = value ?? "default";
55063
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(tenant)) {
55064
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "tenantId must be a bounded exact authority identifier");
55065
+ }
55066
+ return tenant;
55067
+ }
55068
+
55069
+ class PackageOwnedTodosTaskSubtreeTransferAuthority {
55070
+ backend;
55071
+ tenantId;
55072
+ backendOptions;
55073
+ constructor(backend, options = {}) {
55074
+ this.backend = backend;
55075
+ this.tenantId = resolveTenantId2(options.tenantId);
55076
+ this.backendOptions = { ...options, tenantId: this.tenantId };
55077
+ }
55078
+ async capability() {
55079
+ return {
55080
+ authority: "todos",
55081
+ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
55082
+ schema_version: TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION,
55083
+ tenant_id: this.tenantId,
55084
+ backend: this.backend.kind,
55085
+ exact_descendant_closure: true,
55086
+ complete_source_population_digest: true,
55087
+ per_task_version_cas: true,
55088
+ explicit_shared_plan_splits: true,
55089
+ atomic_apply: true,
55090
+ immutable_forward_inverse_receipts: true,
55091
+ prior_image_receipts: true,
55092
+ cas_protected_rollback: true,
55093
+ preserves_descendant_parent_ids: true,
55094
+ preserves_task_and_relation_identities: true
55095
+ };
55096
+ }
55097
+ async inspect(input) {
55098
+ return this.backend.inspect(parseTodosTaskSubtreeTransferInspect(input));
55099
+ }
55100
+ async apply(input) {
55101
+ const request = parseTodosTaskSubtreeTransferApply(input);
55102
+ const expected = deriveTodosTaskSubtreeTransferApplyPreconditionDigest(request);
55103
+ if (request.precondition_digest !== expected) {
55104
+ 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 });
55105
+ }
55106
+ const requestDigest = taskSubtreeTransferRequestDigest(request);
55107
+ const expectedKey = deriveTodosTaskSubtreeTransferIdempotencyKey({
55108
+ operation_id: request.operation_id,
55109
+ step_id: request.step_id,
55110
+ direction: "apply",
55111
+ target_selector: request.root_task_id,
55112
+ request_digest: requestDigest,
55113
+ precondition_digest: request.precondition_digest
55114
+ });
55115
+ if (request.idempotency_key !== expectedKey) {
55116
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the exact transfer request", { expected_idempotency_key: expectedKey });
55117
+ }
55118
+ return this.backend.apply(request, this.backendOptions);
55119
+ }
55120
+ async readExact(receiptIdValue) {
55121
+ return this.backend.readExact(uuid3(receiptIdValue, "receipt_id"));
55122
+ }
55123
+ async rollback(input) {
55124
+ const request = parseTodosTaskSubtreeTransferRollback(input);
55125
+ const requestDigest = taskSubtreeTransferRollbackRequestDigest(request);
55126
+ const expectedKey = deriveTodosTaskSubtreeTransferIdempotencyKey({
55127
+ operation_id: request.operation_id,
55128
+ step_id: request.step_id,
55129
+ direction: "rollback",
55130
+ target_selector: request.receipt_id,
55131
+ request_digest: requestDigest,
55132
+ precondition_digest: request.precondition_digest
55133
+ });
55134
+ if (request.idempotency_key !== expectedKey) {
55135
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_MISMATCH", "rollback idempotency_key does not match the exact rollback request", { expected_idempotency_key: expectedKey });
55136
+ }
55137
+ return this.backend.rollback(request, this.backendOptions);
55138
+ }
55139
+ }
55140
+ function createPostgresTodosTaskSubtreeTransferAuthority(client, options = {}) {
55141
+ return new PackageOwnedTodosTaskSubtreeTransferAuthority(new PostgresTodosTaskSubtreeTransferBackend(client, options), options);
55142
+ }
55143
+ var UUID, DIGEST, INSPECT_FIELDS, APPLY_FIELDS;
55144
+ var init_authority3 = __esm(() => {
55145
+ init_canonical2();
55146
+ init_postgres4();
55147
+ init_sqlite3();
55148
+ init_types6();
55149
+ init_canonical2();
55150
+ 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;
55151
+ DIGEST = /^[0-9a-f]{64}$/i;
55152
+ INSPECT_FIELDS = [
55153
+ "source_project_id",
55154
+ "destination_project_id",
55155
+ "destination_task_list_id",
55156
+ "root_task_id",
55157
+ "destination_parent_id"
55158
+ ];
55159
+ APPLY_FIELDS = [
55160
+ ...INSPECT_FIELDS,
55161
+ "version",
55162
+ "operation_id",
55163
+ "step_id",
55164
+ "idempotency_key",
55165
+ "precondition_digest",
55166
+ "expected_root_parent_id",
55167
+ "source_population_digest",
55168
+ "expected_tasks",
55169
+ "shared_plan_splits"
55170
+ ];
55171
+ });
55172
+
55173
+ // src/task-subtree-transfer/http.ts
55174
+ function json3(body2, status2 = 200) {
55175
+ return new Response(JSON.stringify(body2), { status: status2, headers: JSON_HEADERS3 });
55176
+ }
55177
+ function status2(error) {
55178
+ switch (error.code) {
55179
+ case "TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT":
55180
+ case "TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE":
55181
+ case "TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE":
55182
+ return 400;
55183
+ case "TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND":
55184
+ case "TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND":
55185
+ return 404;
55186
+ case "TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE":
55187
+ return 503;
55188
+ default:
55189
+ return 409;
55190
+ }
55191
+ }
55192
+ async function boundedText2(message, limit) {
55193
+ if (!message.body)
55194
+ return "";
55195
+ const reader = message.body.getReader();
55196
+ const decoder = new TextDecoder;
55197
+ let bytes = 0;
55198
+ let text2 = "";
55199
+ try {
55200
+ while (true) {
55201
+ const { done, value } = await reader.read();
55202
+ if (done)
55203
+ break;
55204
+ bytes += value.byteLength;
55205
+ if (bytes > limit) {
55206
+ await reader.cancel();
55207
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Task-subtree-transfer HTTP body exceeds the byte bound");
55208
+ }
55209
+ text2 += decoder.decode(value, { stream: true });
55210
+ }
55211
+ return text2 + decoder.decode();
55212
+ } finally {
55213
+ reader.releaseLock();
55214
+ }
55215
+ }
55216
+ async function readBody(request) {
55217
+ const declaredLength = Number(request.headers.get("content-length") ?? 0);
55218
+ if (Number.isFinite(declaredLength) && declaredLength > REQUEST_BYTES) {
55219
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Task-subtree-transfer request exceeds the byte bound");
55220
+ }
55221
+ const text2 = await boundedText2(request, REQUEST_BYTES);
55222
+ try {
55223
+ return JSON.parse(text2);
55224
+ } catch {
55225
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Invalid JSON body");
55226
+ }
55227
+ }
55228
+ async function handleTodosTaskSubtreeTransferHttpRequest(request, url, authority, basePath = "/v1/task-subtree-transfer") {
55229
+ if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`))
55230
+ return null;
55231
+ const action = url.pathname.slice(basePath.length).split("/").filter(Boolean).join("/");
55232
+ try {
55233
+ if ((action === "" || action === "capability") && request.method === "GET") {
55234
+ return json3({ capability: await authority.capability() });
55235
+ }
55236
+ if (request.method !== "POST")
55237
+ return json3({ error: "method not allowed" }, 405);
55238
+ if (action === "inspect")
55239
+ return json3({ inspection: await authority.inspect(await readBody(request)) });
55240
+ if (action === "apply")
55241
+ return json3({ result: await authority.apply(await readBody(request)) }, 201);
55242
+ if (action === "rollback")
55243
+ return json3({ result: await authority.rollback(await readBody(request)) }, 201);
55244
+ if (action === "read-exact") {
55245
+ const input = await readBody(request);
55246
+ if (!input || typeof input.receipt_id !== "string") {
55247
+ throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "receipt_id is required");
55248
+ }
55249
+ return json3({ result: await authority.readExact(input.receipt_id) });
55250
+ }
55251
+ return json3({ error: "unknown task-subtree-transfer route" }, 404);
55252
+ } catch (cause) {
55253
+ if (cause instanceof TodosTaskSubtreeTransferError) {
55254
+ return json3({
55255
+ error: cause.message,
55256
+ code: cause.code,
55257
+ details: cause.details,
55258
+ authoritative: true
55259
+ }, status2(cause));
55260
+ }
55261
+ return json3({ error: "internal task-subtree-transfer error" }, 500);
55262
+ }
55263
+ }
55264
+ var JSON_HEADERS3, REQUEST_BYTES, RESPONSE_BYTES;
55265
+ var init_http4 = __esm(() => {
55266
+ init_types6();
55267
+ JSON_HEADERS3 = { "Content-Type": "application/json" };
55268
+ REQUEST_BYTES = 2 * 1024 * 1024;
55269
+ RESPONSE_BYTES = 8 * 1024 * 1024;
55270
+ });
55271
+
55272
+ // src/task-subtree-transfer/index.ts
55273
+ var init_task_subtree_transfer = __esm(() => {
55274
+ init_authority3();
55275
+ init_http4();
55276
+ init_types6();
55277
+ });
55278
+
54194
55279
  // src/storage/comment-redaction-backfill.ts
54195
55280
  function assertSafeIdentifier2(value) {
54196
55281
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
@@ -54288,6 +55373,207 @@ var init_comment_redaction_backfill = __esm(() => {
54288
55373
  init_postgres_sync();
54289
55374
  });
54290
55375
 
55376
+ // src/storage/timestamp-backfill.ts
55377
+ function toIso(value) {
55378
+ if (!value || value === "null")
55379
+ return null;
55380
+ const parsed = new Date(value);
55381
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
55382
+ }
55383
+ async function backfillMissingTimestamps(client, options = {}) {
55384
+ const service = options.service ?? "todos";
55385
+ const tableName = options.tableName ?? "todos_sync_records";
55386
+ const batchSize = Math.min(Math.max(options.batchSize ?? 100, 1), 500);
55387
+ const apply = Boolean(options.apply);
55388
+ if (apply && options.confirmation !== TIMESTAMP_BACKFILL_CONFIRMATION) {
55389
+ throw new Error(`timestamp-backfill: --apply requires --confirm=${TIMESTAMP_BACKFILL_CONFIRMATION}`);
55390
+ }
55391
+ if (apply && !options.evidencePath) {
55392
+ throw new Error("timestamp-backfill: --apply requires --evidence-path so the pre-change state is preserved before mutation");
55393
+ }
55394
+ let scanned = 0;
55395
+ let candidates = 0;
55396
+ let completedAtBackfilled = 0;
55397
+ let startedAtBackfilled = 0;
55398
+ let remainingAfterApply = 0;
55399
+ let batches = 0;
55400
+ let cursor = null;
55401
+ for (;; ) {
55402
+ const pageResult = await client.query(`/* todos:timestamp-backfill-candidates */ SELECT object_id,
55403
+ payload->>'status' AS status,
55404
+ payload->>'started_at' AS started_at,
55405
+ payload->>'completed_at' AS completed_at,
55406
+ payload->>'updated_at' AS updated_at,
55407
+ payload->'metadata'->'_failure'->>'failed_at' AS failed_at
55408
+ FROM ${tableName}
55409
+ WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL
55410
+ AND ((payload->>'status' = 'completed'
55411
+ AND ((payload->>'completed_at') IS NULL OR (payload->>'completed_at') = 'null'))
55412
+ OR (payload->>'status' = 'failed'
55413
+ AND ((payload->>'started_at') IS NULL OR (payload->>'started_at') = 'null'
55414
+ OR (payload->>'completed_at') IS NULL OR (payload->>'completed_at') = 'null'))
55415
+ OR (payload->>'status' = 'cancelled'
55416
+ AND ((payload->>'started_at') IS NULL OR (payload->>'started_at') = 'null'
55417
+ OR (payload->>'completed_at') IS NULL OR (payload->>'completed_at') = 'null')))
55418
+ AND ($2::text IS NULL OR object_id > $2)
55419
+ ORDER BY object_id
55420
+ LIMIT $3`, [service, cursor, batchSize]);
55421
+ const page = pageResult.rows;
55422
+ batches += 1;
55423
+ scanned += page.length;
55424
+ if (page.length === 0)
55425
+ break;
55426
+ const batch = page.map((row) => ({
55427
+ ...row,
55428
+ started_at: toIso(row.started_at),
55429
+ completed_at: toIso(row.completed_at),
55430
+ updated_at: toIso(row.updated_at),
55431
+ failed_at: toIso(row.failed_at)
55432
+ }));
55433
+ const taskIds = batch.map((row) => row.object_id);
55434
+ const historyResult = await client.query(`/* todos:timestamp-backfill-history */ SELECT
55435
+ payload->>'task_id' AS task_id,
55436
+ payload->>'action' AS action,
55437
+ payload->>'new_value' AS new_value,
55438
+ payload->>'created_at' AS created_at
55439
+ FROM ${tableName}
55440
+ WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
55441
+ AND payload->>'action' IN ('start', 'complete', 'fail')
55442
+ AND payload->>'task_id' = ANY($2::text[])`, [service, taskIds]);
55443
+ const historyRows = historyResult.rows;
55444
+ const byTask = new Map;
55445
+ for (const row of historyRows) {
55446
+ if (!row.task_id)
55447
+ continue;
55448
+ const list = byTask.get(row.task_id) ?? [];
55449
+ list.push(row);
55450
+ byTask.set(row.task_id, list);
55451
+ }
55452
+ for (const row of batch) {
55453
+ candidates += 1;
55454
+ const entries = (byTask.get(row.object_id) ?? []).sort((a, b) => (a.created_at ?? "").localeCompare(b.created_at ?? ""));
55455
+ let derivedStartedAt = null;
55456
+ let startedSource = null;
55457
+ if (!row.started_at) {
55458
+ const start = entries.find((entry2) => entry2.action === "start");
55459
+ const candidate = toIso(start?.created_at);
55460
+ if (candidate) {
55461
+ derivedStartedAt = candidate;
55462
+ startedSource = "audit_history";
55463
+ }
55464
+ }
55465
+ let derivedCompletedAt = null;
55466
+ let completedSource = null;
55467
+ if (!row.completed_at) {
55468
+ const terminal = entries.filter((entry2) => entry2.action === "complete" || entry2.action === "fail");
55469
+ if (terminal.length > 0) {
55470
+ const latest = terminal[terminal.length - 1];
55471
+ const candidate = toIso(latest.created_at);
55472
+ if (candidate) {
55473
+ derivedCompletedAt = candidate;
55474
+ completedSource = "audit_history";
55475
+ }
55476
+ }
55477
+ if (!derivedCompletedAt && row.failed_at && row.status === "failed") {
55478
+ const candidate = toIso(row.failed_at);
55479
+ if (candidate) {
55480
+ derivedCompletedAt = candidate;
55481
+ completedSource = "failure_metadata";
55482
+ }
55483
+ }
55484
+ if (!derivedCompletedAt) {
55485
+ const candidate = toIso(row.updated_at);
55486
+ if (candidate) {
55487
+ derivedCompletedAt = candidate;
55488
+ completedSource = "updated_at";
55489
+ }
55490
+ }
55491
+ }
55492
+ const evidenceRow = {
55493
+ object_id: row.object_id,
55494
+ status: row.status,
55495
+ started_at: row.started_at,
55496
+ completed_at: row.completed_at,
55497
+ updated_at: row.updated_at,
55498
+ derived_started_at: derivedStartedAt,
55499
+ derived_completed_at: derivedCompletedAt,
55500
+ started_source: startedSource,
55501
+ completed_source: completedSource
55502
+ };
55503
+ if (apply && options.evidencePath) {
55504
+ const { appendFileSync: appendFileSync2, mkdirSync: mkdirSync8 } = await import("fs");
55505
+ const { dirname: dirname9 } = await import("path");
55506
+ mkdirSync8(dirname9(options.evidencePath), { recursive: true });
55507
+ appendFileSync2(options.evidencePath, JSON.stringify(evidenceRow) + `
55508
+ `);
55509
+ }
55510
+ let completedAtLanded = false;
55511
+ let startedAtLanded = false;
55512
+ if (apply) {
55513
+ const willFillStarted = row.started_at === null && derivedStartedAt !== null;
55514
+ const willFillCompleted = row.completed_at === null && derivedCompletedAt !== null;
55515
+ if (willFillStarted || willFillCompleted) {
55516
+ const result = await client.query(`/* todos:timestamp-backfill-apply */ UPDATE ${tableName} SET
55517
+ payload = jsonb_set(
55518
+ jsonb_set(
55519
+ payload,
55520
+ '{completed_at}',
55521
+ CASE WHEN $3::text IS NOT NULL
55522
+ AND ((payload->>'completed_at') IS NULL OR (payload->>'completed_at') = 'null')
55523
+ THEN to_jsonb($3::text) ELSE payload->'completed_at' END,
55524
+ true
55525
+ ),
55526
+ '{started_at}',
55527
+ CASE WHEN $4::text IS NOT NULL
55528
+ AND ((payload->>'started_at') IS NULL OR (payload->>'started_at') = 'null')
55529
+ THEN to_jsonb($4::text) ELSE payload->'started_at' END,
55530
+ true
55531
+ )
55532
+ WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL
55533
+ AND (($3::text IS NOT NULL
55534
+ AND ((payload->>'completed_at') IS NULL OR (payload->>'completed_at') = 'null'))
55535
+ OR ($4::text IS NOT NULL
55536
+ AND ((payload->>'started_at') IS NULL OR (payload->>'started_at') = 'null')))
55537
+ RETURNING object_id,
55538
+ ($3::text IS NOT NULL AND payload->>'completed_at' = $3::text) AS completed_written,
55539
+ ($4::text IS NOT NULL AND payload->>'started_at' = $4::text) AS started_written`, [service, row.object_id, derivedCompletedAt, derivedStartedAt]);
55540
+ if (result.rows.length > 0) {
55541
+ const landed = result.rows[0];
55542
+ completedAtLanded = willFillCompleted && landed.completed_written;
55543
+ startedAtLanded = willFillStarted && landed.started_written;
55544
+ if (completedAtLanded)
55545
+ completedAtBackfilled += 1;
55546
+ if (startedAtLanded)
55547
+ startedAtBackfilled += 1;
55548
+ }
55549
+ }
55550
+ }
55551
+ if (row.completed_at === null && !completedAtLanded) {
55552
+ remainingAfterApply += 1;
55553
+ } else if ((row.status === "failed" || row.status === "cancelled") && row.started_at === null && !startedAtLanded) {
55554
+ remainingAfterApply += 1;
55555
+ }
55556
+ }
55557
+ const last = batch[batch.length - 1];
55558
+ if (last.object_id === cursor)
55559
+ break;
55560
+ cursor = last.object_id;
55561
+ if (page.length < batchSize)
55562
+ break;
55563
+ }
55564
+ return {
55565
+ dry_run: !apply,
55566
+ scanned,
55567
+ candidates,
55568
+ completed_at_backfilled: apply ? completedAtBackfilled : 0,
55569
+ started_at_backfilled: apply ? startedAtBackfilled : 0,
55570
+ batches,
55571
+ remaining_candidates: apply ? remainingAfterApply : candidates,
55572
+ evidence_path: options.evidencePath ?? null
55573
+ };
55574
+ }
55575
+ var TIMESTAMP_BACKFILL_CONFIRMATION = "BACKFILL_TODOS_TERMINAL_TIMESTAMPS";
55576
+
54291
55577
  // src/server/cloud.ts
54292
55578
  var exports_cloud = {};
54293
55579
  __export(exports_cloud, {
@@ -54296,8 +55582,8 @@ __export(exports_cloud, {
54296
55582
  pingCloud: () => pingCloud,
54297
55583
  normalizeCloudPayloads: () => normalizeCloudPayloads,
54298
55584
  isPostgresBackendConfigured: () => isPostgresBackendConfigured,
54299
- isCloudModeEnabled: () => isCloudModeEnabled,
54300
55585
  getCloudVerifier: () => getCloudVerifier,
55586
+ getCloudTaskSubtreeTransferAuthority: () => getCloudTaskSubtreeTransferAuthority,
54301
55587
  getCloudTaskManifestAuthority: () => getCloudTaskManifestAuthority,
54302
55588
  getCloudStorageAdapter: () => getCloudStorageAdapter,
54303
55589
  getCloudProjectRegistrationAuthority: () => getCloudProjectRegistrationAuthority,
@@ -54309,6 +55595,7 @@ __export(exports_cloud, {
54309
55595
  ensureCloudSchema: () => ensureCloudSchema,
54310
55596
  ensureCloudCommentCursorIndex: () => ensureCloudCommentCursorIndex,
54311
55597
  closeCloud: () => closeCloud,
55598
+ backfillCloudTimestamps: () => backfillCloudTimestamps,
54312
55599
  backfillCloudCommentRedaction: () => backfillCloudCommentRedaction,
54313
55600
  TODOS_APP_SLUG: () => TODOS_APP_SLUG
54314
55601
  });
@@ -54323,9 +55610,6 @@ function resolveSigningSecret(env = process.env) {
54323
55610
  function isPostgresBackendConfigured(env = process.env) {
54324
55611
  return Boolean(resolveCloudDatabaseUrl(env));
54325
55612
  }
54326
- function isCloudModeEnabled(env = process.env) {
54327
- return isPostgresBackendConfigured(env);
54328
- }
54329
55613
  function getCloudTenantId() {
54330
55614
  return process.env.HASNA_TODOS_TENANT_ID ?? "default";
54331
55615
  }
@@ -54372,6 +55656,15 @@ function getCloudTaskManifestAuthority() {
54372
55656
  });
54373
55657
  return cachedTaskManifestAuthority;
54374
55658
  }
55659
+ function getCloudTaskSubtreeTransferAuthority() {
55660
+ if (cachedTaskSubtreeTransferAuthority)
55661
+ return cachedTaskSubtreeTransferAuthority;
55662
+ cachedTaskSubtreeTransferAuthority = createPostgresTodosTaskSubtreeTransferAuthority(getClient(), {
55663
+ service: TODOS_APP_SLUG,
55664
+ tenantId: getCloudTenantId()
55665
+ });
55666
+ return cachedTaskSubtreeTransferAuthority;
55667
+ }
54375
55668
  function authClient() {
54376
55669
  const client = getClient();
54377
55670
  return {
@@ -54405,7 +55698,7 @@ function getCloudVerifier() {
54405
55698
  cachedVerifier = verifyApiKey2({
54406
55699
  app: TODOS_APP_SLUG,
54407
55700
  signingSecret,
54408
- isRevoked: store.isRevoked
55701
+ keyStatus: store.keyStatus
54409
55702
  });
54410
55703
  return cachedVerifier;
54411
55704
  }
@@ -54426,6 +55719,9 @@ async function ensureCloudSchema() {
54426
55719
  for (const sql of postgresTodosTaskManifestSchemaSql(getCloudTenantId())) {
54427
55720
  await client.query(sql);
54428
55721
  }
55722
+ for (const sql of postgresTodosTaskSubtreeTransferSchemaSql()) {
55723
+ await client.query(sql);
55724
+ }
54429
55725
  await getApiKeyStore().ensureSchema();
54430
55726
  })();
54431
55727
  return schemaEnsured;
@@ -54453,6 +55749,9 @@ async function normalizeCloudPayloads() {
54453
55749
  function backfillCloudCommentRedaction(options = {}) {
54454
55750
  return backfillPostgresCommentRedaction(getClient(), { ...options, service: TODOS_APP_SLUG });
54455
55751
  }
55752
+ function backfillCloudTimestamps(options = {}) {
55753
+ return backfillMissingTimestamps(getClient(), { ...options, service: TODOS_APP_SLUG });
55754
+ }
54456
55755
  async function pingCloud() {
54457
55756
  const client = getClient();
54458
55757
  const res = await client.query("select 1 as ok");
@@ -54468,9 +55767,10 @@ async function closeCloud() {
54468
55767
  cachedPrGroupLedger = null;
54469
55768
  cachedProjectRegistrationAuthority = null;
54470
55769
  cachedTaskManifestAuthority = null;
55770
+ cachedTaskSubtreeTransferAuthority = null;
54471
55771
  schemaEnsured = null;
54472
55772
  }
54473
- var TODOS_APP_SLUG = "todos", cachedClient = null, cachedAdapter = null, cachedStore = null, cachedVerifier = null, cachedPrGroupLedger = null, cachedProjectRegistrationAuthority = null, cachedTaskManifestAuthority = null, schemaEnsured = null;
55773
+ var TODOS_APP_SLUG = "todos", cachedClient = null, cachedAdapter = null, cachedStore = null, cachedVerifier = null, cachedPrGroupLedger = null, cachedProjectRegistrationAuthority = null, cachedTaskManifestAuthority = null, cachedTaskSubtreeTransferAuthority = null, schemaEnsured = null;
54474
55774
  var init_cloud = __esm(() => {
54475
55775
  init_cloud_client();
54476
55776
  init_postgres_adapter();
@@ -54478,6 +55778,7 @@ var init_cloud = __esm(() => {
54478
55778
  init_postgres();
54479
55779
  init_project_registration();
54480
55780
  init_task_manifest();
55781
+ init_task_subtree_transfer();
54481
55782
  init_postgres_sync();
54482
55783
  init_comment_redaction_backfill();
54483
55784
  });
@@ -54691,8 +55992,8 @@ function rowToOrg(row) {
54691
55992
  function createOrg(input, db) {
54692
55993
  const d = db || getDatabase();
54693
55994
  const id = uuid();
54694
- const timestamp5 = now();
54695
- 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]);
55995
+ const timestamp6 = now();
55996
+ d.run(`INSERT INTO orgs (id, name, description, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, [id, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp6, timestamp6]);
54696
55997
  return getOrg(id, d);
54697
55998
  }
54698
55999
  function getOrg(id, db) {
@@ -54836,9 +56137,9 @@ function parseBoundedLimit(value, fallback, max) {
54836
56137
  return fallback;
54837
56138
  return Math.min(parsed, max);
54838
56139
  }
54839
- function mapTaskError(e, json4) {
56140
+ function mapTaskError(e, json5) {
54840
56141
  if (e instanceof VersionConflictError) {
54841
- return json4({
56142
+ return json5({
54842
56143
  error: e.message,
54843
56144
  code: VersionConflictError.code,
54844
56145
  expected_version: e.expectedVersion,
@@ -54846,23 +56147,23 @@ function mapTaskError(e, json4) {
54846
56147
  }, 409);
54847
56148
  }
54848
56149
  if (e instanceof TaskNotFoundError) {
54849
- return json4({ error: e.message, code: TaskNotFoundError.code }, 404);
56150
+ return json5({ error: e.message, code: TaskNotFoundError.code }, 404);
54850
56151
  }
54851
56152
  if (e instanceof LockError) {
54852
- return json4({ error: e.message, code: LockError.code }, 409);
56153
+ return json5({ error: e.message, code: LockError.code }, 409);
54853
56154
  }
54854
56155
  if (e instanceof CompletionGuardError) {
54855
- return json4({
56156
+ return json5({
54856
56157
  error: e.message,
54857
56158
  code: CompletionGuardError.code,
54858
56159
  retry_after: e.retryAfterSeconds ?? null
54859
56160
  }, 409);
54860
56161
  }
54861
56162
  if (e instanceof TaskNotStartableError) {
54862
- return json4({ error: e.message, code: TaskNotStartableError.code }, 409);
56163
+ return json5({ error: e.message, code: TaskNotStartableError.code }, 409);
54863
56164
  }
54864
56165
  if (e instanceof Error && / is blocked by /.test(e.message)) {
54865
- return json4({ error: e.message, code: "TASK_NOT_STARTABLE" }, 409);
56166
+ return json5({ error: e.message, code: "TASK_NOT_STARTABLE" }, 409);
54866
56167
  }
54867
56168
  return null;
54868
56169
  }
@@ -54947,11 +56248,11 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
54947
56248
  }
54948
56249
  });
54949
56250
  }
54950
- function handleHealth(_ctx, json4) {
56251
+ function handleHealth(_ctx, json5) {
54951
56252
  const stats2 = getTaskStats();
54952
56253
  const staleCount = getStaleTasks(30).length;
54953
56254
  const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
54954
- return json4({
56255
+ return json5({
54955
56256
  status: staleCount === 0 && overdueRecurring === 0 ? "ok" : "warn",
54956
56257
  tasks: stats2.total,
54957
56258
  stale: staleCount,
@@ -54959,18 +56260,18 @@ function handleHealth(_ctx, json4) {
54959
56260
  timestamp: new Date().toISOString()
54960
56261
  });
54961
56262
  }
54962
- function handleHeadlessBoundary(_ctx, json4) {
56263
+ function handleHeadlessBoundary(_ctx, json5) {
54963
56264
  const { getHeadlessBoundaryManifest: getHeadlessBoundaryManifest2 } = (init_headless_boundaries(), __toCommonJS(exports_headless_boundaries));
54964
- return json4(getHeadlessBoundaryManifest2());
56265
+ return json5(getHeadlessBoundaryManifest2());
54965
56266
  }
54966
- function handleStats(_ctx, json4) {
56267
+ function handleStats(_ctx, json5) {
54967
56268
  const stats2 = getTaskStats();
54968
56269
  const byStatus = stats2.by_status;
54969
56270
  const projects = listProjects();
54970
56271
  const agents = listAgents();
54971
56272
  const staleCount = getStaleTasks(30).length;
54972
56273
  const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
54973
- return json4({
56274
+ return json5({
54974
56275
  total_tasks: stats2.total,
54975
56276
  pending: byStatus["pending"] ?? 0,
54976
56277
  in_progress: byStatus["in_progress"] ?? 0,
@@ -54993,10 +56294,10 @@ function taskStatusQueryParam(url) {
54993
56294
  return { ok: false, message: result.message };
54994
56295
  return { ok: true, value: collapseEnumValues(result.values) };
54995
56296
  }
54996
- async function handleListTasks(_req, url, _ctx, json4, taskToSummary2) {
56297
+ async function handleListTasks(_req, url, _ctx, json5, taskToSummary2) {
54997
56298
  const statusParam = taskStatusQueryParam(url);
54998
56299
  if (!statusParam.ok)
54999
- return json4({ error: statusParam.message }, 400);
56300
+ return json5({ error: statusParam.message }, 400);
55000
56301
  const projectId = url.searchParams.get("project_id") || undefined;
55001
56302
  const sessionId = url.searchParams.get("session_id") || undefined;
55002
56303
  const agentId = url.searchParams.get("agent_id") || undefined;
@@ -55011,13 +56312,13 @@ async function handleListTasks(_req, url, _ctx, json4, taskToSummary2) {
55011
56312
  limit: limitParam ? parseInt(limitParam, 10) : undefined,
55012
56313
  offset: offsetParam ? parseInt(offsetParam, 10) : undefined
55013
56314
  });
55014
- return json4(tasks.map((t) => taskToSummary2(t, fields)));
56315
+ return json5(tasks.map((t) => taskToSummary2(t, fields)));
55015
56316
  }
55016
- async function handleCreateTask(req, ctx, json4, taskToSummary2) {
56317
+ async function handleCreateTask(req, ctx, json5, taskToSummary2) {
55017
56318
  try {
55018
56319
  const body2 = await req.json();
55019
56320
  if (!body2.title)
55020
- return json4({ error: "Missing 'title'" }, 400);
56321
+ return json5({ error: "Missing 'title'" }, 400);
55021
56322
  const createdBy = body2.created_by ?? body2.agent_id ?? "dashboard";
55022
56323
  const task3 = createTask({
55023
56324
  title: body2.title,
@@ -55029,19 +56330,19 @@ async function handleCreateTask(req, ctx, json4, taskToSummary2) {
55029
56330
  ...body2.assigned_to ? { assigned_to: body2.assigned_to } : {}
55030
56331
  });
55031
56332
  ctx.broadcastEvent({ type: "task", task_id: task3.id, action: "created", agent_id: task3.agent_id, project_id: task3.project_id });
55032
- return json4(taskToSummary2(task3), 201);
56333
+ return json5(taskToSummary2(task3), 201);
55033
56334
  } catch (e) {
55034
- return json4({ error: e instanceof Error ? e.message : "Failed to create task" }, 500);
56335
+ return json5({ error: e instanceof Error ? e.message : "Failed to create task" }, 500);
55035
56336
  }
55036
56337
  }
55037
- async function handleUpsertTask(req, ctx, json4, taskToSummary2) {
56338
+ async function handleUpsertTask(req, ctx, json5, taskToSummary2) {
55038
56339
  try {
55039
56340
  const body2 = await req.json();
55040
56341
  if (typeof body2["fingerprint"] !== "string" || body2["fingerprint"].trim() === "") {
55041
- return json4({ error: "Missing 'fingerprint'" }, 400);
56342
+ return json5({ error: "Missing 'fingerprint'" }, 400);
55042
56343
  }
55043
56344
  if (typeof body2["title"] !== "string" || body2["title"].trim() === "") {
55044
- return json4({ error: "Missing 'title'" }, 400);
56345
+ return json5({ error: "Missing 'title'" }, 400);
55045
56346
  }
55046
56347
  const metadata = body2["metadata"] && typeof body2["metadata"] === "object" && !Array.isArray(body2["metadata"]) ? { ...body2["metadata"] } : {};
55047
56348
  for (const key2 of ["expectation_id", "expectation_fingerprint", "evidence_paths", "origin_loop_id", "origin_run_id", "expected", "observed", "acceptance"]) {
@@ -55062,9 +56363,9 @@ async function handleUpsertTask(req, ctx, json4, taskToSummary2) {
55062
56363
  metadata
55063
56364
  });
55064
56365
  ctx.broadcastEvent({ type: "task", task_id: result.task.id, action: result.created ? "created" : "updated", agent_id: result.task.agent_id, project_id: result.task.project_id });
55065
- return json4({ created: result.created, task: taskToSummary2(result.task) }, result.created ? 201 : 200);
56366
+ return json5({ created: result.created, task: taskToSummary2(result.task) }, result.created ? 201 : 200);
55066
56367
  } catch (e) {
55067
- return json4({ error: e instanceof Error ? e.message : "Failed to upsert task" }, 500);
56368
+ return json5({ error: e instanceof Error ? e.message : "Failed to upsert task" }, 500);
55068
56369
  }
55069
56370
  }
55070
56371
  function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
@@ -55116,11 +56417,11 @@ function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
55116
56417
  }
55117
56418
  });
55118
56419
  }
55119
- async function handleTasksBulk(req, _ctx, json4) {
56420
+ async function handleTasksBulk(req, _ctx, json5) {
55120
56421
  try {
55121
56422
  const body2 = await req.json();
55122
56423
  if (!body2.ids?.length || !body2.action)
55123
- return json4({ error: "Missing ids or action" }, 400);
56424
+ return json5({ error: "Missing ids or action" }, 400);
55124
56425
  const results = [];
55125
56426
  for (const id of body2.ids) {
55126
56427
  try {
@@ -55138,84 +56439,84 @@ async function handleTasksBulk(req, _ctx, json4) {
55138
56439
  results.push({ id, success: false, error: e instanceof Error ? e.message : "Failed" });
55139
56440
  }
55140
56441
  }
55141
- return json4({ results, succeeded: results.filter((r) => r.success).length, failed: results.filter((r) => !r.success).length });
56442
+ return json5({ results, succeeded: results.filter((r) => r.success).length, failed: results.filter((r) => !r.success).length });
55142
56443
  } catch (e) {
55143
- return json4({ error: e instanceof Error ? e.message : "Failed" }, 500);
56444
+ return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
55144
56445
  }
55145
56446
  }
55146
- function handleTasksStatus(_req, url, _ctx, json4) {
56447
+ function handleTasksStatus(_req, url, _ctx, json5) {
55147
56448
  try {
55148
56449
  const projectId = url.searchParams.get("project_id") || undefined;
55149
56450
  const agentId = url.searchParams.get("agent_id") || undefined;
55150
- const status2 = getStatus(projectId ? { project_id: projectId } : undefined, agentId);
55151
- return json4(status2);
56451
+ const status3 = getStatus(projectId ? { project_id: projectId } : undefined, agentId);
56452
+ return json5(status3);
55152
56453
  } catch (e) {
55153
- return json4({ error: e instanceof Error ? e.message : "Failed" }, 500);
56454
+ return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
55154
56455
  }
55155
56456
  }
55156
- function handleTasksNext(_req, url, _ctx, json4, taskToSummary2) {
56457
+ function handleTasksNext(_req, url, _ctx, json5, taskToSummary2) {
55157
56458
  try {
55158
56459
  const projectId = url.searchParams.get("project_id") || undefined;
55159
56460
  const agentId = url.searchParams.get("agent_id") || undefined;
55160
56461
  const fields = parseFieldsParam(url);
55161
56462
  const task3 = getNextTask(agentId, projectId ? { project_id: projectId } : undefined);
55162
- return json4({ task: task3 ? taskToSummary2(task3, fields) : null });
56463
+ return json5({ task: task3 ? taskToSummary2(task3, fields) : null });
55163
56464
  } catch (e) {
55164
- return json4({ error: e instanceof Error ? e.message : "Failed" }, 500);
56465
+ return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
55165
56466
  }
55166
56467
  }
55167
- function handleTasksActive(_req, url, _ctx, json4) {
56468
+ function handleTasksActive(_req, url, _ctx, json5) {
55168
56469
  try {
55169
56470
  const projectId = url.searchParams.get("project_id") || undefined;
55170
56471
  const work = getActiveWork(projectId ? { project_id: projectId } : undefined);
55171
- return json4({ active: work, count: work.length });
56472
+ return json5({ active: work, count: work.length });
55172
56473
  } catch (e) {
55173
- return json4({ error: e instanceof Error ? e.message : "Failed" }, 500);
56474
+ return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
55174
56475
  }
55175
56476
  }
55176
- function handleTasksStale(_req, url, _ctx, json4, taskToSummary2) {
56477
+ function handleTasksStale(_req, url, _ctx, json5, taskToSummary2) {
55177
56478
  try {
55178
56479
  const projectId = url.searchParams.get("project_id") || undefined;
55179
56480
  const minutes2 = parseInt(url.searchParams.get("minutes") || "30", 10);
55180
56481
  const fields = parseFieldsParam(url);
55181
56482
  const tasks = getStaleTasks(minutes2, projectId ? { project_id: projectId } : undefined);
55182
- return json4({ tasks: tasks.map((t) => taskToSummary2(t, fields)), count: tasks.length });
56483
+ return json5({ tasks: tasks.map((t) => taskToSummary2(t, fields)), count: tasks.length });
55183
56484
  } catch (e) {
55184
- return json4({ error: e instanceof Error ? e.message : "Failed" }, 500);
56485
+ return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
55185
56486
  }
55186
56487
  }
55187
- function handleTasksChanged(_req, url, _ctx, json4, taskToSummary2) {
56488
+ function handleTasksChanged(_req, url, _ctx, json5, taskToSummary2) {
55188
56489
  try {
55189
56490
  const since = url.searchParams.get("since");
55190
56491
  if (!since)
55191
- return json4({ error: "since parameter required (ISO date string)" }, 400);
56492
+ return json5({ error: "since parameter required (ISO date string)" }, 400);
55192
56493
  const projectId = url.searchParams.get("project_id") || undefined;
55193
56494
  const fields = parseFieldsParam(url);
55194
56495
  const tasks = getTasksChangedSince(since, projectId ? { project_id: projectId } : undefined);
55195
- return json4({ tasks: tasks.map((t) => taskToSummary2(t, fields)), count: tasks.length, since });
56496
+ return json5({ tasks: tasks.map((t) => taskToSummary2(t, fields)), count: tasks.length, since });
55196
56497
  } catch (e) {
55197
- return json4({ error: e instanceof Error ? e.message : "Failed" }, 500);
56498
+ return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
55198
56499
  }
55199
56500
  }
55200
- function handleTasksContext(_req, url, _ctx, json4, taskToSummary2) {
56501
+ function handleTasksContext(_req, url, _ctx, json5, taskToSummary2) {
55201
56502
  const agentId = url.searchParams.get("agent_id") || undefined;
55202
56503
  const projectId = url.searchParams.get("project_id") || undefined;
55203
56504
  const format = url.searchParams.get("format") || "text";
55204
56505
  const fields = parseFieldsParam(url);
55205
56506
  const filters = projectId ? { project_id: projectId } : undefined;
55206
- const status2 = getStatus(filters, agentId);
56507
+ const status3 = getStatus(filters, agentId);
55207
56508
  const next = getNextTask(agentId, filters);
55208
56509
  if (format === "json") {
55209
- return json4({ status: status2, next_task: next ? taskToSummary2(next, fields) : null });
56510
+ return json5({ status: status3, next_task: next ? taskToSummary2(next, fields) : null });
55210
56511
  }
55211
56512
  const lines = [];
55212
- lines.push(`Tasks: ${status2.pending} pending | ${status2.in_progress} active | ${status2.completed} done`);
55213
- if (status2.stale_count > 0)
55214
- lines.push(`${status2.stale_count} stale tasks stuck in-progress`);
55215
- if (status2.overdue_recurring > 0)
55216
- lines.push(`${status2.overdue_recurring} overdue recurring tasks`);
55217
- if (status2.active_work.length > 0) {
55218
- lines.push(`Active: ${status2.active_work.slice(0, 3).map((w) => `${w.short_id || w.id.slice(0, 8)} (${w.assigned_to || "?"})`).join(", ")}`);
56513
+ lines.push(`Tasks: ${status3.pending} pending | ${status3.in_progress} active | ${status3.completed} done`);
56514
+ if (status3.stale_count > 0)
56515
+ lines.push(`${status3.stale_count} stale tasks stuck in-progress`);
56516
+ if (status3.overdue_recurring > 0)
56517
+ lines.push(`${status3.overdue_recurring} overdue recurring tasks`);
56518
+ if (status3.active_work.length > 0) {
56519
+ lines.push(`Active: ${status3.active_work.slice(0, 3).map((w) => `${w.short_id || w.id.slice(0, 8)} (${w.assigned_to || "?"})`).join(", ")}`);
55219
56520
  }
55220
56521
  if (next)
55221
56522
  lines.push(`Next up: ${next.short_id || next.id.slice(0, 8)} [${next.priority}] ${next.title}`);
@@ -55223,18 +56524,18 @@ function handleTasksContext(_req, url, _ctx, json4, taskToSummary2) {
55223
56524
  `);
55224
56525
  return new Response(text2, { headers: { "Content-Type": "text/plain" } });
55225
56526
  }
55226
- function handleTaskAttachments(id, _ctx, json4) {
56527
+ function handleTaskAttachments(id, _ctx, json5) {
55227
56528
  const task3 = getTask(id);
55228
56529
  if (!task3)
55229
- return json4({ error: "Task not found" }, 404);
56530
+ return json5({ error: "Task not found" }, 404);
55230
56531
  const evidence = task3.metadata?._evidence || {};
55231
56532
  const attachmentIds = evidence.attachments || [];
55232
- return json4({ task_id: id, short_id: task3.short_id, attachment_ids: attachmentIds, count: attachmentIds.length, files_changed: evidence.files_changed, commit_hash: evidence.commit_hash, notes: evidence.notes });
56533
+ return json5({ task_id: id, short_id: task3.short_id, attachment_ids: attachmentIds, count: attachmentIds.length, files_changed: evidence.files_changed, commit_hash: evidence.commit_hash, notes: evidence.notes });
55233
56534
  }
55234
- async function handleTaskProgress(id, req, method, _ctx, json4, url) {
56535
+ async function handleTaskProgress(id, req, method, _ctx, json5, url) {
55235
56536
  const task3 = getTask(id);
55236
56537
  if (!task3)
55237
- return json4({ error: "Task not found" }, 404);
56538
+ return json5({ error: "Task not found" }, 404);
55238
56539
  if (method === "GET") {
55239
56540
  const all = listComments(id);
55240
56541
  const progress = all.filter((c) => c.type === "progress");
@@ -55242,7 +56543,7 @@ async function handleTaskProgress(id, req, method, _ctx, json4, url) {
55242
56543
  const format = url?.searchParams.get("format") || "compact";
55243
56544
  const limit = parseBoundedLimit(url?.searchParams.get("limit") || null, 20, 200);
55244
56545
  const progressEntries = format === "full" ? progress : progress.slice(-limit);
55245
- return json4({
56546
+ return json5({
55246
56547
  task_id: id,
55247
56548
  progress_entries: progressEntries,
55248
56549
  latest,
@@ -55259,27 +56560,27 @@ async function handleTaskProgress(id, req, method, _ctx, json4, url) {
55259
56560
  try {
55260
56561
  const body2 = await req.json();
55261
56562
  if (!body2.message)
55262
- return json4({ error: "message required" }, 400);
56563
+ return json5({ error: "message required" }, 400);
55263
56564
  const comment2 = logProgress(id, body2.message, body2.pct_complete, body2.agent_id);
55264
- return json4(comment2, 201);
56565
+ return json5(comment2, 201);
55265
56566
  } catch (e) {
55266
- return json4({ error: e instanceof Error ? e.message : "Failed to log progress" }, 500);
56567
+ return json5({ error: e instanceof Error ? e.message : "Failed to log progress" }, 500);
55267
56568
  }
55268
56569
  }
55269
56570
  return null;
55270
56571
  }
55271
- function handleGetTask(id, _ctx, json4, taskToSummary2, url) {
56572
+ function handleGetTask(id, _ctx, json5, taskToSummary2, url) {
55272
56573
  const task3 = getTask(id);
55273
56574
  if (!task3)
55274
- return json4({ error: "Task not found" }, 404);
55275
- return json4(taskToSummary2(task3, url ? parseFieldsParam(url) : undefined));
56575
+ return json5({ error: "Task not found" }, 404);
56576
+ return json5(taskToSummary2(task3, url ? parseFieldsParam(url) : undefined));
55276
56577
  }
55277
- async function handlePatchTask(id, req, _ctx, json4, taskToSummary2) {
56578
+ async function handlePatchTask(id, req, _ctx, json5, taskToSummary2) {
55278
56579
  try {
55279
56580
  const body2 = await req.json();
55280
56581
  const task3 = getTask(id);
55281
56582
  if (!task3)
55282
- return json4({ error: "Task not found" }, 404);
56583
+ return json5({ error: "Task not found" }, 404);
55283
56584
  const ALLOWED = new Set(["title", "description", "status", "priority", "assigned_to", "plan_id", "task_list_id", "tags", "metadata", "due_at", "estimated_minutes", "actual_minutes", "confidence", "retry_count", "max_retries", "retry_after", "task_type"]);
55284
56585
  const safeBody = {};
55285
56586
  for (const [key2, value] of Object.entries(body2)) {
@@ -55291,85 +56592,85 @@ async function handlePatchTask(id, req, _ctx, json4, taskToSummary2) {
55291
56592
  ...safeBody,
55292
56593
  version: clientVersion
55293
56594
  });
55294
- return json4(taskToSummary2(updated));
56595
+ return json5(taskToSummary2(updated));
55295
56596
  } catch (e) {
55296
- const mapped = mapTaskError(e, json4);
56597
+ const mapped = mapTaskError(e, json5);
55297
56598
  if (mapped)
55298
56599
  return mapped;
55299
- return json4({ error: e instanceof Error ? e.message : "Failed to update task" }, 500);
56600
+ return json5({ error: e instanceof Error ? e.message : "Failed to update task" }, 500);
55300
56601
  }
55301
56602
  }
55302
- function handleDeleteTask(id, _ctx, json4) {
56603
+ function handleDeleteTask(id, _ctx, json5) {
55303
56604
  const deleted = deleteTask(id);
55304
56605
  if (!deleted)
55305
- return json4({ error: "Task not found" }, 404);
55306
- return json4({ success: true });
56606
+ return json5({ error: "Task not found" }, 404);
56607
+ return json5({ success: true });
55307
56608
  }
55308
- function handleStartTask(id, ctx, json4, taskToSummary2) {
56609
+ function handleStartTask(id, ctx, json5, taskToSummary2) {
55309
56610
  try {
55310
56611
  const task3 = startTask(id, "dashboard");
55311
56612
  ctx.broadcastEvent({ type: "task", task_id: task3.id, action: "started", agent_id: "dashboard", project_id: task3.project_id });
55312
- return json4(taskToSummary2(task3));
56613
+ return json5(taskToSummary2(task3));
55313
56614
  } catch (e) {
55314
- const mapped = mapTaskError(e, json4);
56615
+ const mapped = mapTaskError(e, json5);
55315
56616
  if (mapped)
55316
56617
  return mapped;
55317
- return json4({ error: e instanceof Error ? e.message : "Failed to start task" }, 500);
56618
+ return json5({ error: e instanceof Error ? e.message : "Failed to start task" }, 500);
55318
56619
  }
55319
56620
  }
55320
- async function handleFailTask(id, req, ctx, json4, taskToSummary2) {
56621
+ async function handleFailTask(id, req, ctx, json5, taskToSummary2) {
55321
56622
  try {
55322
56623
  const body2 = await req.json().catch(() => ({}));
55323
56624
  const result = failTask(id, body2.agent_id, body2.reason, { retry: body2.retry, error_code: body2.error_code });
55324
56625
  ctx.broadcastEvent({ type: "task", task_id: id, action: "failed", agent_id: body2.agent_id || null, project_id: result.task.project_id });
55325
- return json4({ task: taskToSummary2(result.task), retry_task: result.retryTask ? taskToSummary2(result.retryTask) : null });
56626
+ return json5({ task: taskToSummary2(result.task), retry_task: result.retryTask ? taskToSummary2(result.retryTask) : null });
55326
56627
  } catch (e) {
55327
- return json4({ error: e instanceof Error ? e.message : "Failed to fail task" }, 500);
56628
+ return json5({ error: e instanceof Error ? e.message : "Failed to fail task" }, 500);
55328
56629
  }
55329
56630
  }
55330
- function handleCompleteTask(id, ctx, json4, taskToSummary2) {
56631
+ function handleCompleteTask(id, ctx, json5, taskToSummary2) {
55331
56632
  try {
55332
56633
  const task3 = completeTask(id, "dashboard");
55333
56634
  ctx.broadcastEvent({ type: "task", task_id: task3.id, action: "completed", agent_id: "dashboard", project_id: task3.project_id });
55334
- return json4(taskToSummary2(task3));
56635
+ return json5(taskToSummary2(task3));
55335
56636
  } catch (e) {
55336
- const mapped = mapTaskError(e, json4);
56637
+ const mapped = mapTaskError(e, json5);
55337
56638
  if (mapped)
55338
56639
  return mapped;
55339
- return json4({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500);
56640
+ return json5({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500);
55340
56641
  }
55341
56642
  }
55342
- function handleListProjects(url, _ctx, json4) {
56643
+ function handleListProjects(url, _ctx, json5) {
55343
56644
  const pFieldsParam = url.searchParams.get("fields");
55344
56645
  const pFields = pFieldsParam ? pFieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
55345
56646
  const projects = listProjects();
55346
- return json4(pFields ? projects.map((p) => Object.fromEntries(pFields.map((f) => [f, p[f] ?? null]))) : projects);
56647
+ return json5(pFields ? projects.map((p) => Object.fromEntries(pFields.map((f) => [f, p[f] ?? null]))) : projects);
55347
56648
  }
55348
- async function handleCreateProject(req, _ctx, json4) {
56649
+ async function handleCreateProject(req, _ctx, json5) {
55349
56650
  try {
55350
56651
  const body2 = await req.json();
55351
56652
  if (!body2.name || !body2.path)
55352
- return json4({ error: "Missing name or path" }, 400);
56653
+ return json5({ error: "Missing name or path" }, 400);
55353
56654
  const project = createProject({ name: body2.name, path: body2.path, description: body2.description });
55354
- return json4(project, 201);
56655
+ return json5(project, 201);
55355
56656
  } catch (e) {
55356
- return json4({ error: e instanceof Error ? e.message : "Failed to create project" }, 500);
56657
+ return json5({ error: e instanceof Error ? e.message : "Failed to create project" }, 500);
55357
56658
  }
55358
56659
  }
55359
- function handleDeleteProject(id, _ctx, json4) {
56660
+ function handleDeleteProject(id, _ctx, json5) {
55360
56661
  const deleted = deleteProject(id);
55361
56662
  if (!deleted)
55362
- return json4({ error: "Project not found" }, 404);
55363
- return json4({ success: true });
56663
+ return json5({ error: "Project not found" }, 404);
56664
+ return json5({ success: true });
55364
56665
  }
55365
- async function handleAgentMe(_req, url, _ctx, json4, taskToSummary2) {
56666
+ async function handleAgentMe(_req, url, _ctx, json5, taskToSummary2) {
55366
56667
  try {
55367
56668
  const name = url.searchParams.get("name");
55368
56669
  if (!name)
55369
- return json4({ error: "Missing name param" }, 400);
56670
+ return json5({ error: "Missing name param" }, 400);
55370
56671
  const agentResult = registerAgent({ name });
55371
56672
  if (isAgentConflict(agentResult))
55372
- return json4({ error: agentResult.message, conflict: true }, 409);
56673
+ return json5({ error: agentResult.message, conflict: true }, 409);
55373
56674
  const agent = agentResult;
55374
56675
  const tasks = listTasks({ assigned_to: agent.name });
55375
56676
  const agentIdTasks = listTasks({ agent_id: agent.id });
@@ -55377,7 +56678,7 @@ async function handleAgentMe(_req, url, _ctx, json4, taskToSummary2) {
55377
56678
  const pending = allTasks.filter((t) => t.status === "pending");
55378
56679
  const inProgress = allTasks.filter((t) => t.status === "in_progress");
55379
56680
  const completed = allTasks.filter((t) => t.status === "completed");
55380
- return json4({
56681
+ return json5({
55381
56682
  agent,
55382
56683
  pending_tasks: pending.map((t) => taskToSummary2(t)),
55383
56684
  in_progress_tasks: inProgress.map((t) => taskToSummary2(t)),
@@ -55391,132 +56692,132 @@ async function handleAgentMe(_req, url, _ctx, json4, taskToSummary2) {
55391
56692
  });
55392
56693
  } catch (e) {
55393
56694
  if (e instanceof InvalidAgentNameError)
55394
- return json4({ error: e.message, suggestions: e.suggestions }, 400);
55395
- return json4({ error: e instanceof Error ? e.message : "Failed to get agent profile" }, 500);
56695
+ return json5({ error: e.message, suggestions: e.suggestions }, 400);
56696
+ return json5({ error: e instanceof Error ? e.message : "Failed to get agent profile" }, 500);
55396
56697
  }
55397
56698
  }
55398
- function handleAgentQueue(agentId, _ctx, json4, taskToSummary2) {
56699
+ function handleAgentQueue(agentId, _ctx, json5, taskToSummary2) {
55399
56700
  const aliasSet = assignedToAliasSet(getDatabase(), agentId);
55400
56701
  const pending = listTasks({ status: "pending" });
55401
56702
  const queue = pending.filter((t) => aliasSet.has((t.assigned_to ?? "").toLowerCase()) || t.agent_id === agentId || !t.assigned_to && !t.locked_by);
55402
56703
  const order = { critical: 0, high: 1, medium: 2, low: 3 };
55403
56704
  queue.sort((a, b) => (order[a.priority] ?? 4) - (order[b.priority] ?? 4) || new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
55404
- return json4(queue.map((t) => taskToSummary2(t)));
56705
+ return json5(queue.map((t) => taskToSummary2(t)));
55405
56706
  }
55406
- async function handleClaimTask(req, _ctx, json4, taskToSummary2) {
56707
+ async function handleClaimTask(req, _ctx, json5, taskToSummary2) {
55407
56708
  try {
55408
56709
  const body2 = await req.json();
55409
56710
  const agentId = body2.agent_id || "anonymous";
55410
56711
  const task3 = claimNextTask(agentId, body2.project_id ? { project_id: body2.project_id } : undefined);
55411
- return json4({ task: task3 ? taskToSummary2(task3) : null });
56712
+ return json5({ task: task3 ? taskToSummary2(task3) : null });
55412
56713
  } catch (e) {
55413
- return json4({ error: e instanceof Error ? e.message : "Failed to claim" }, 500);
56714
+ return json5({ error: e instanceof Error ? e.message : "Failed to claim" }, 500);
55414
56715
  }
55415
56716
  }
55416
- function handleListOrgs(_ctx, json4) {
55417
- return json4(listOrgs());
56717
+ function handleListOrgs(_ctx, json5) {
56718
+ return json5(listOrgs());
55418
56719
  }
55419
- async function handleCreateOrg(req, _ctx, json4) {
56720
+ async function handleCreateOrg(req, _ctx, json5) {
55420
56721
  try {
55421
56722
  const body2 = await req.json();
55422
56723
  if (!body2.name)
55423
- return json4({ error: "Missing name" }, 400);
55424
- return json4(createOrg(body2), 201);
56724
+ return json5({ error: "Missing name" }, 400);
56725
+ return json5(createOrg(body2), 201);
55425
56726
  } catch (e) {
55426
- return json4({ error: e instanceof Error ? e.message : "Failed" }, 500);
56727
+ return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
55427
56728
  }
55428
56729
  }
55429
- async function handleUpdateOrg(id, req, _ctx, json4) {
56730
+ async function handleUpdateOrg(id, req, _ctx, json5) {
55430
56731
  try {
55431
56732
  const body2 = await req.json();
55432
- return json4(updateOrg(id, body2));
56733
+ return json5(updateOrg(id, body2));
55433
56734
  } catch (e) {
55434
- return json4({ error: e instanceof Error ? e.message : "Failed" }, 500);
56735
+ return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
55435
56736
  }
55436
56737
  }
55437
- function handleDeleteOrg(id, _ctx, json4) {
56738
+ function handleDeleteOrg(id, _ctx, json5) {
55438
56739
  const deleted = deleteOrg(id);
55439
- return json4(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404);
56740
+ return json5(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404);
55440
56741
  }
55441
- function handleOrgChart(_ctx, json4) {
55442
- return json4(getOrgChart());
56742
+ function handleOrgChart(_ctx, json5) {
56743
+ return json5(getOrgChart());
55443
56744
  }
55444
- function handleAgentTeam(agentId, _ctx, json4) {
55445
- return json4(getDirectReports(decodeURIComponent(agentId)));
56745
+ function handleAgentTeam(agentId, _ctx, json5) {
56746
+ return json5(getDirectReports(decodeURIComponent(agentId)));
55446
56747
  }
55447
- function handleListAgents(url, _ctx, json4) {
56748
+ function handleListAgents(url, _ctx, json5) {
55448
56749
  const aFieldsParam = url.searchParams.get("fields");
55449
56750
  const aFields = aFieldsParam ? aFieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
55450
56751
  const agents = listAgents();
55451
- return json4(aFields ? agents.map((a) => Object.fromEntries(aFields.map((f) => [f, a[f] ?? null]))) : agents);
56752
+ return json5(aFields ? agents.map((a) => Object.fromEntries(aFields.map((f) => [f, a[f] ?? null]))) : agents);
55452
56753
  }
55453
- async function handleRegisterAgent(req, _ctx, json4) {
56754
+ async function handleRegisterAgent(req, _ctx, json5) {
55454
56755
  try {
55455
56756
  const body2 = await req.json();
55456
56757
  if (!body2.name)
55457
- return json4({ error: "Missing name" }, 400);
56758
+ return json5({ error: "Missing name" }, 400);
55458
56759
  const result = registerAgent({ name: body2.name, description: body2.description, session_id: body2.session_id, working_dir: body2.working_dir });
55459
56760
  if (isAgentConflict(result))
55460
- return json4({ error: result.message, conflict: true }, 409);
55461
- return json4(result, 201);
56761
+ return json5({ error: result.message, conflict: true }, 409);
56762
+ return json5(result, 201);
55462
56763
  } catch (e) {
55463
56764
  if (e instanceof InvalidAgentNameError)
55464
- return json4({ error: e.message, suggestions: e.suggestions }, 400);
55465
- return json4({ error: e instanceof Error ? e.message : "Failed to register agent" }, 500);
56765
+ return json5({ error: e.message, suggestions: e.suggestions }, 400);
56766
+ return json5({ error: e instanceof Error ? e.message : "Failed to register agent" }, 500);
55466
56767
  }
55467
56768
  }
55468
- async function handleUpdateAgent(id, req, _ctx, json4) {
56769
+ async function handleUpdateAgent(id, req, _ctx, json5) {
55469
56770
  try {
55470
56771
  const body2 = await req.json();
55471
56772
  const agent = updateAgent(id, body2);
55472
- return json4(agent);
56773
+ return json5(agent);
55473
56774
  } catch (e) {
55474
56775
  if (e instanceof InvalidAgentNameError)
55475
- return json4({ error: e.message, suggestions: e.suggestions }, 400);
55476
- return json4({ error: e instanceof Error ? e.message : "Failed to update agent" }, 500);
56776
+ return json5({ error: e.message, suggestions: e.suggestions }, 400);
56777
+ return json5({ error: e instanceof Error ? e.message : "Failed to update agent" }, 500);
55477
56778
  }
55478
56779
  }
55479
- function handleDeleteAgent(id, _ctx, json4) {
56780
+ function handleDeleteAgent(id, _ctx, json5) {
55480
56781
  const deleted = deleteAgent(id);
55481
56782
  if (!deleted)
55482
- return json4({ error: "Agent not found" }, 404);
55483
- return json4({ success: true });
56783
+ return json5({ error: "Agent not found" }, 404);
56784
+ return json5({ success: true });
55484
56785
  }
55485
- async function handleBulkDeleteAgents(req, _ctx, json4) {
56786
+ async function handleBulkDeleteAgents(req, _ctx, json5) {
55486
56787
  try {
55487
56788
  const body2 = await req.json();
55488
56789
  if (!body2.ids?.length || body2.action !== "delete")
55489
- return json4({ error: "Missing ids or invalid action" }, 400);
56790
+ return json5({ error: "Missing ids or invalid action" }, 400);
55490
56791
  let succeeded = 0;
55491
56792
  for (const id of body2.ids) {
55492
56793
  if (deleteAgent(id))
55493
56794
  succeeded++;
55494
56795
  }
55495
- return json4({ succeeded, failed: body2.ids.length - succeeded });
56796
+ return json5({ succeeded, failed: body2.ids.length - succeeded });
55496
56797
  } catch (e) {
55497
- return json4({ error: e instanceof Error ? e.message : "Failed" }, 500);
56798
+ return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
55498
56799
  }
55499
56800
  }
55500
- async function handleBulkDeleteProjects(req, _ctx, json4) {
56801
+ async function handleBulkDeleteProjects(req, _ctx, json5) {
55501
56802
  try {
55502
56803
  const body2 = await req.json();
55503
56804
  if (!body2.ids?.length || body2.action !== "delete")
55504
- return json4({ error: "Missing ids or invalid action" }, 400);
56805
+ return json5({ error: "Missing ids or invalid action" }, 400);
55505
56806
  let succeeded = 0;
55506
56807
  for (const id of body2.ids) {
55507
56808
  if (deleteProject(id))
55508
56809
  succeeded++;
55509
56810
  }
55510
- return json4({ succeeded, failed: body2.ids.length - succeeded });
56811
+ return json5({ succeeded, failed: body2.ids.length - succeeded });
55511
56812
  } catch (e) {
55512
- return json4({ error: e instanceof Error ? e.message : "Failed" }, 500);
56813
+ return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
55513
56814
  }
55514
56815
  }
55515
- function handleDoctor(_ctx, json4) {
56816
+ function handleDoctor(_ctx, json5) {
55516
56817
  const { runTodosDoctor: runTodosDoctor2 } = (init_doctor(), __toCommonJS(exports_doctor));
55517
- return json4(runTodosDoctor2({ apply: false }));
56818
+ return json5(runTodosDoctor2({ apply: false }));
55518
56819
  }
55519
- function handleReport(_req, url, _ctx, json4) {
56820
+ function handleReport(_req, url, _ctx, json5) {
55520
56821
  const days = parseInt(url.searchParams.get("days") || "7", 10);
55521
56822
  const projectId = url.searchParams.get("project_id") || undefined;
55522
56823
  const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
@@ -55532,62 +56833,62 @@ function handleReport(_req, url, _ctx, json4) {
55532
56833
  byDay[day] = (byDay[day] || 0) + 1;
55533
56834
  }
55534
56835
  const completionRate = changed.length > 0 ? Math.round(completed.length / changed.length * 100) : 0;
55535
- return json4({ days, period_since: since, total: all.length, stats: stats2, changed: changed.length, completed: completed.length, failed: failed.length, completion_rate: completionRate, by_day: byDay });
56836
+ return json5({ days, period_since: since, total: all.length, stats: stats2, changed: changed.length, completed: completed.length, failed: failed.length, completion_rate: completionRate, by_day: byDay });
55536
56837
  }
55537
- function handleActivity(_req, url, _ctx, json4) {
56838
+ function handleActivity(_req, url, _ctx, json5) {
55538
56839
  const limit = parseInt(url.searchParams.get("limit") || "50", 10);
55539
- return json4(getRecentActivity(limit));
56840
+ return json5(getRecentActivity(limit));
55540
56841
  }
55541
- function handleTaskHistory(id, _ctx, json4, url) {
56842
+ function handleTaskHistory(id, _ctx, json5, url) {
55542
56843
  const history = getTaskHistory(id);
55543
56844
  const format = url?.searchParams.get("format") || "compact";
55544
56845
  const limit = parseBoundedLimit(url?.searchParams.get("limit") || null, 20, 500);
55545
- return json4(format === "full" ? history : history.slice(0, limit));
56846
+ return json5(format === "full" ? history : history.slice(0, limit));
55546
56847
  }
55547
- function handleListWebhooks(_ctx, json4) {
55548
- return json4(listWebhooks());
56848
+ function handleListWebhooks(_ctx, json5) {
56849
+ return json5(listWebhooks());
55549
56850
  }
55550
- async function handleCreateWebhook(req, _ctx, json4) {
56851
+ async function handleCreateWebhook(req, _ctx, json5) {
55551
56852
  try {
55552
56853
  const body2 = await req.json();
55553
56854
  if (!body2.url)
55554
- return json4({ error: "Missing url" }, 400);
55555
- return json4(createWebhook(body2), 201);
56855
+ return json5({ error: "Missing url" }, 400);
56856
+ return json5(createWebhook(body2), 201);
55556
56857
  } catch (e) {
55557
- return json4({ error: e instanceof Error ? e.message : "Failed" }, 500);
56858
+ return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
55558
56859
  }
55559
56860
  }
55560
- function handleDeleteWebhook(id, _ctx, json4) {
56861
+ function handleDeleteWebhook(id, _ctx, json5) {
55561
56862
  const deleted = deleteWebhook(id);
55562
- return json4(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404);
56863
+ return json5(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404);
55563
56864
  }
55564
- function handleListTemplates(_ctx, json4) {
55565
- return json4(listTemplates());
56865
+ function handleListTemplates(_ctx, json5) {
56866
+ return json5(listTemplates());
55566
56867
  }
55567
- async function handleCreateTemplate(req, _ctx, json4) {
56868
+ async function handleCreateTemplate(req, _ctx, json5) {
55568
56869
  try {
55569
56870
  const body2 = await req.json();
55570
56871
  if (!body2.name || !body2.title_pattern)
55571
- return json4({ error: "Missing name or title_pattern" }, 400);
55572
- return json4(createTemplate(body2), 201);
56872
+ return json5({ error: "Missing name or title_pattern" }, 400);
56873
+ return json5(createTemplate(body2), 201);
55573
56874
  } catch (e) {
55574
- return json4({ error: e instanceof Error ? e.message : "Failed" }, 500);
56875
+ return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
55575
56876
  }
55576
56877
  }
55577
- function handleDeleteTemplate(id, _ctx, json4) {
56878
+ function handleDeleteTemplate(id, _ctx, json5) {
55578
56879
  const deleted = deleteTemplate(id);
55579
- return json4(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404);
56880
+ return json5(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404);
55580
56881
  }
55581
- function handleListPlans(url, _ctx, json4) {
56882
+ function handleListPlans(url, _ctx, json5) {
55582
56883
  const projectId = url.searchParams.get("project_id") || undefined;
55583
56884
  const plans = listPlans(projectId);
55584
- return json4(plans);
56885
+ return json5(plans);
55585
56886
  }
55586
- async function handleCreatePlan(req, _ctx, json4) {
56887
+ async function handleCreatePlan(req, _ctx, json5) {
55587
56888
  try {
55588
56889
  const body2 = await req.json();
55589
56890
  if (!body2.name)
55590
- return json4({ error: "Missing 'name'" }, 400);
56891
+ return json5({ error: "Missing 'name'" }, 400);
55591
56892
  const plan = createPlan({
55592
56893
  name: body2.name,
55593
56894
  slug: body2.slug,
@@ -55597,49 +56898,49 @@ async function handleCreatePlan(req, _ctx, json4) {
55597
56898
  agent_id: body2.agent_id,
55598
56899
  status: body2.status
55599
56900
  });
55600
- return json4(plan, 201);
56901
+ return json5(plan, 201);
55601
56902
  } catch (e) {
55602
- return json4({ error: e instanceof Error ? e.message : "Failed to create plan" }, 500);
56903
+ return json5({ error: e instanceof Error ? e.message : "Failed to create plan" }, 500);
55603
56904
  }
55604
56905
  }
55605
- async function handleBulkDeletePlans(req, _ctx, json4) {
56906
+ async function handleBulkDeletePlans(req, _ctx, json5) {
55606
56907
  try {
55607
56908
  const body2 = await req.json();
55608
56909
  if (!body2.ids?.length || body2.action !== "delete")
55609
- return json4({ error: "Missing ids or invalid action" }, 400);
56910
+ return json5({ error: "Missing ids or invalid action" }, 400);
55610
56911
  let succeeded = 0;
55611
56912
  for (const id of body2.ids) {
55612
56913
  if (deletePlan(id))
55613
56914
  succeeded++;
55614
56915
  }
55615
- return json4({ succeeded, failed: body2.ids.length - succeeded });
56916
+ return json5({ succeeded, failed: body2.ids.length - succeeded });
55616
56917
  } catch (e) {
55617
- return json4({ error: e instanceof Error ? e.message : "Failed" }, 500);
56918
+ return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
55618
56919
  }
55619
56920
  }
55620
- function handleGetPlan(id, _ctx, json4, taskToSummary2) {
56921
+ function handleGetPlan(id, _ctx, json5, taskToSummary2) {
55621
56922
  const plan = getPlan(id);
55622
56923
  if (!plan)
55623
- return json4({ error: "Plan not found" }, 404);
56924
+ return json5({ error: "Plan not found" }, 404);
55624
56925
  const tasks = listTasks({ plan_id: id });
55625
- return json4({ ...plan, tasks: tasks.map((t) => taskToSummary2(t)) });
56926
+ return json5({ ...plan, tasks: tasks.map((t) => taskToSummary2(t)) });
55626
56927
  }
55627
- async function handleUpdatePlan(id, req, _ctx, json4) {
56928
+ async function handleUpdatePlan(id, req, _ctx, json5) {
55628
56929
  try {
55629
56930
  const body2 = await req.json();
55630
56931
  const plan = updatePlan(id, body2);
55631
- return json4(plan);
56932
+ return json5(plan);
55632
56933
  } catch (e) {
55633
- return json4({ error: e instanceof Error ? e.message : "Failed to update plan" }, 500);
56934
+ return json5({ error: e instanceof Error ? e.message : "Failed to update plan" }, 500);
55634
56935
  }
55635
56936
  }
55636
- function handleDeletePlan(id, _ctx, json4) {
56937
+ function handleDeletePlan(id, _ctx, json5) {
55637
56938
  const deleted = deletePlan(id);
55638
56939
  if (!deleted)
55639
- return json4({ error: "Plan not found" }, 404);
55640
- return json4({ success: true });
56940
+ return json5({ error: "Plan not found" }, 404);
56941
+ return json5({ success: true });
55641
56942
  }
55642
- function handleStaticFiles(path, method, ctx, json4, serveStaticFile2) {
56943
+ function handleStaticFiles(path, method, ctx, json5, serveStaticFile2) {
55643
56944
  if (!ctx.dashboardExists || method !== "GET" && method !== "HEAD")
55644
56945
  return null;
55645
56946
  if (path !== "/") {
@@ -55647,7 +56948,7 @@ function handleStaticFiles(path, method, ctx, json4, serveStaticFile2) {
55647
56948
  const resolvedFile = resolve16(filePath);
55648
56949
  const resolvedBase = resolve16(ctx.dashboardDir);
55649
56950
  if (!resolvedFile.startsWith(resolvedBase + sep3) && resolvedFile !== resolvedBase) {
55650
- return json4({ error: "Forbidden" }, 403);
56951
+ return json5({ error: "Forbidden" }, 403);
55651
56952
  }
55652
56953
  const res2 = serveStaticFile2(filePath);
55653
56954
  if (res2)
@@ -55703,6 +57004,19 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
55703
57004
  TaskManifestReceipt: taskManifestReceiptSchema,
55704
57005
  TaskManifestApplyResult: taskManifestApplyResultSchema,
55705
57006
  TaskManifestApplyResponse: taskManifestApplyResponseSchema,
57007
+ TaskSubtreeTransferExpectedTask: taskSubtreeTransferExpectedTaskSchema,
57008
+ TaskSubtreeTransferSharedPlanSplit: taskSubtreeTransferSharedPlanSplitSchema,
57009
+ TaskSubtreeTransferInspectRequest: taskSubtreeTransferInspectRequestSchema,
57010
+ TaskSubtreeTransferInspection: taskSubtreeTransferInspectionSchema,
57011
+ TaskSubtreeTransferApplyRequest: taskSubtreeTransferApplyRequestSchema,
57012
+ TaskSubtreeTransferTaskImage: taskSubtreeTransferTaskImageSchema,
57013
+ TaskSubtreeTransferPlanImage: taskSubtreeTransferPlanImageSchema,
57014
+ TaskSubtreeTransferImage: taskSubtreeTransferImageSchema,
57015
+ TaskSubtreeTransferReceipt: taskSubtreeTransferReceiptSchema,
57016
+ TaskSubtreeTransferResult: taskSubtreeTransferResultSchema,
57017
+ TaskSubtreeTransferCapability: taskSubtreeTransferCapabilitySchema,
57018
+ TaskSubtreeTransferRollbackRequest: taskSubtreeTransferRollbackRequestSchema,
57019
+ TaskSubtreeTransferReadExactRequest: taskSubtreeTransferReadExactRequestSchema,
55706
57020
  TaskManifestCompensateRequest: taskManifestCompensateRequestSchema,
55707
57021
  TaskManifestCompensationResult: taskManifestCompensationResultSchema,
55708
57022
  TaskManifestCompensateResponse: taskManifestCompensateResponseSchema,
@@ -55907,7 +57221,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
55907
57221
  slug: { type: "string", minLength: 1, pattern: ".*[A-Za-z0-9].*" },
55908
57222
  name: { type: "string" },
55909
57223
  description: { type: "string" },
55910
- metadata: { type: "object", additionalProperties: true }
57224
+ metadata: { type: "object", additionalProperties: true },
57225
+ project_id: { type: "string", nullable: true }
55911
57226
  }
55912
57227
  },
55913
57228
  CreateTaskCommentInput: {
@@ -57051,6 +58366,160 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
57051
58366
  }
57052
58367
  }
57053
58368
  },
58369
+ "/v1/task-subtree-transfer/capability": {
58370
+ get: {
58371
+ operationId: "getTaskSubtreeTransferCapability",
58372
+ summary: "Read the current task-subtree-transfer authority capability and tenant",
58373
+ responses: {
58374
+ "200": {
58375
+ content: {
58376
+ "application/json": {
58377
+ schema: {
58378
+ type: "object",
58379
+ additionalProperties: false,
58380
+ required: ["capability"],
58381
+ properties: {
58382
+ capability: { $ref: "#/components/schemas/TaskSubtreeTransferCapability" }
58383
+ }
58384
+ }
58385
+ }
58386
+ }
58387
+ },
58388
+ "401": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
58389
+ "503": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
58390
+ }
58391
+ }
58392
+ },
58393
+ "/v1/task-subtree-transfer/inspect": {
58394
+ post: {
58395
+ operationId: "inspectTaskSubtreeTransfer",
58396
+ summary: "Inspect one exact task subtree and complete source population snapshot",
58397
+ requestBody: {
58398
+ required: true,
58399
+ content: {
58400
+ "application/json": {
58401
+ schema: { $ref: "#/components/schemas/TaskSubtreeTransferInspectRequest" }
58402
+ }
58403
+ }
58404
+ },
58405
+ responses: {
58406
+ "200": {
58407
+ content: {
58408
+ "application/json": {
58409
+ schema: {
58410
+ type: "object",
58411
+ additionalProperties: false,
58412
+ required: ["inspection"],
58413
+ properties: {
58414
+ inspection: { $ref: "#/components/schemas/TaskSubtreeTransferInspection" }
58415
+ }
58416
+ }
58417
+ }
58418
+ }
58419
+ },
58420
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
58421
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
58422
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
58423
+ }
58424
+ }
58425
+ },
58426
+ "/v1/task-subtree-transfer/apply": {
58427
+ post: {
58428
+ operationId: "applyTaskSubtreeTransfer",
58429
+ summary: "Apply one exact task subtree transfer atomically with CAS and immutable receipt",
58430
+ requestBody: {
58431
+ required: true,
58432
+ content: {
58433
+ "application/json": {
58434
+ schema: { $ref: "#/components/schemas/TaskSubtreeTransferApplyRequest" }
58435
+ }
58436
+ }
58437
+ },
58438
+ responses: {
58439
+ "201": {
58440
+ content: {
58441
+ "application/json": {
58442
+ schema: {
58443
+ type: "object",
58444
+ additionalProperties: false,
58445
+ required: ["result"],
58446
+ properties: {
58447
+ result: { $ref: "#/components/schemas/TaskSubtreeTransferResult" }
58448
+ }
58449
+ }
58450
+ }
58451
+ }
58452
+ },
58453
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
58454
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
58455
+ "503": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
58456
+ }
58457
+ }
58458
+ },
58459
+ "/v1/task-subtree-transfer/read-exact": {
58460
+ post: {
58461
+ operationId: "readExactTaskSubtreeTransfer",
58462
+ summary: "Read one exact immutable task-subtree-transfer receipt",
58463
+ requestBody: {
58464
+ required: true,
58465
+ content: {
58466
+ "application/json": {
58467
+ schema: { $ref: "#/components/schemas/TaskSubtreeTransferReadExactRequest" }
58468
+ }
58469
+ }
58470
+ },
58471
+ responses: {
58472
+ "200": {
58473
+ content: {
58474
+ "application/json": {
58475
+ schema: {
58476
+ type: "object",
58477
+ additionalProperties: false,
58478
+ required: ["result"],
58479
+ properties: {
58480
+ result: { $ref: "#/components/schemas/TaskSubtreeTransferResult" }
58481
+ }
58482
+ }
58483
+ }
58484
+ }
58485
+ },
58486
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
58487
+ }
58488
+ }
58489
+ },
58490
+ "/v1/task-subtree-transfer/rollback": {
58491
+ post: {
58492
+ operationId: "rollbackTaskSubtreeTransfer",
58493
+ summary: "Rollback one exact task-subtree-transfer receipt with CAS protection",
58494
+ requestBody: {
58495
+ required: true,
58496
+ content: {
58497
+ "application/json": {
58498
+ schema: { $ref: "#/components/schemas/TaskSubtreeTransferRollbackRequest" }
58499
+ }
58500
+ }
58501
+ },
58502
+ responses: {
58503
+ "201": {
58504
+ content: {
58505
+ "application/json": {
58506
+ schema: {
58507
+ type: "object",
58508
+ additionalProperties: false,
58509
+ required: ["result"],
58510
+ properties: {
58511
+ result: { $ref: "#/components/schemas/TaskSubtreeTransferResult" }
58512
+ }
58513
+ }
58514
+ }
58515
+ }
58516
+ },
58517
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
58518
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
58519
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
58520
+ }
58521
+ }
58522
+ },
57054
58523
  "/v1/task-manifest/capability": {
57055
58524
  get: {
57056
58525
  operationId: "getTaskManifestCapability",
@@ -58004,7 +59473,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
58004
59473
  }
58005
59474
  });
58006
59475
  }
58007
- var taskSchema, taskManifestBoundsSchema, taskManifestCapabilitySchema, taskManifestCapabilityResponseSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestSchema, taskManifestReceiptSchema, taskManifestApplyResultSchema, taskManifestApplyResponseSchema, taskManifestCompensateRequestSchema, taskManifestCompensationResultSchema, taskManifestCompensateResponseSchema, taskManifestReadExactRequestSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema, projectRegistrationBoundsProperties, projectRegistrationReceiptSchema, projectRegistrationCapabilitySchema, projectRegistrationRequestSchema, projectRegistrationLookupRequestSchema, priorRegistrationAdoptionValidationSchema, projectResourceSchema, projectResourcePageSchema;
59476
+ var taskSchema, taskManifestBoundsSchema, taskManifestCapabilitySchema, taskManifestCapabilityResponseSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestSchema, taskManifestReceiptSchema, taskManifestApplyResultSchema, taskManifestApplyResponseSchema, taskSubtreeTransferExpectedTaskSchema, taskSubtreeTransferSharedPlanSplitSchema, taskSubtreeTransferInspectRequestSchema, taskSubtreeTransferInspectionSchema, taskSubtreeTransferApplyRequestSchema, taskSubtreeTransferTaskImageSchema, taskSubtreeTransferPlanImageSchema, taskSubtreeTransferImageSchema, taskSubtreeTransferReceiptSchema, taskSubtreeTransferResultSchema, taskSubtreeTransferCapabilitySchema, taskSubtreeTransferRollbackRequestSchema, taskSubtreeTransferReadExactRequestSchema, taskManifestCompensateRequestSchema, taskManifestCompensationResultSchema, taskManifestCompensateResponseSchema, taskManifestReadExactRequestSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema, projectRegistrationBoundsProperties, projectRegistrationReceiptSchema, projectRegistrationCapabilitySchema, projectRegistrationRequestSchema, projectRegistrationLookupRequestSchema, priorRegistrationAdoptionValidationSchema, projectResourceSchema, projectResourcePageSchema;
58008
59477
  var init_openapi = __esm(() => {
58009
59478
  init_package_version();
58010
59479
  init_types();
@@ -58282,6 +59751,288 @@ var init_openapi = __esm(() => {
58282
59751
  required: ["result"],
58283
59752
  properties: { result: { $ref: "#/components/schemas/TaskManifestApplyResult" } }
58284
59753
  };
59754
+ taskSubtreeTransferExpectedTaskSchema = {
59755
+ type: "object",
59756
+ additionalProperties: false,
59757
+ required: ["task_id", "version"],
59758
+ properties: {
59759
+ task_id: { type: "string", format: "uuid" },
59760
+ version: { type: "integer", minimum: 1 }
59761
+ }
59762
+ };
59763
+ taskSubtreeTransferSharedPlanSplitSchema = {
59764
+ type: "object",
59765
+ additionalProperties: false,
59766
+ required: ["source_plan_id", "destination_plan_id"],
59767
+ properties: {
59768
+ source_plan_id: { type: "string", format: "uuid" },
59769
+ destination_plan_id: { type: "string", format: "uuid" }
59770
+ }
59771
+ };
59772
+ taskSubtreeTransferInspectRequestSchema = {
59773
+ type: "object",
59774
+ additionalProperties: false,
59775
+ required: [
59776
+ "source_project_id",
59777
+ "destination_project_id",
59778
+ "destination_task_list_id",
59779
+ "root_task_id",
59780
+ "destination_parent_id"
59781
+ ],
59782
+ properties: {
59783
+ source_project_id: { type: "string", format: "uuid" },
59784
+ destination_project_id: { type: "string", format: "uuid" },
59785
+ destination_task_list_id: { type: "string", format: "uuid" },
59786
+ root_task_id: { type: "string", format: "uuid" },
59787
+ destination_parent_id: { type: "string", format: "uuid", nullable: true }
59788
+ }
59789
+ };
59790
+ taskSubtreeTransferInspectionSchema = {
59791
+ type: "object",
59792
+ additionalProperties: false,
59793
+ required: [
59794
+ "source_project_id",
59795
+ "destination_project_id",
59796
+ "destination_task_list_id",
59797
+ "root_task_id",
59798
+ "destination_parent_id",
59799
+ "expected_root_parent_id",
59800
+ "source_population_digest",
59801
+ "expected_tasks",
59802
+ "contained_plan_ids",
59803
+ "shared_plan_ids",
59804
+ "complete"
59805
+ ],
59806
+ properties: {
59807
+ ...taskSubtreeTransferInspectRequestSchema.properties,
59808
+ expected_root_parent_id: { type: "string", format: "uuid", nullable: true },
59809
+ source_population_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
59810
+ expected_tasks: {
59811
+ type: "array",
59812
+ minItems: 1,
59813
+ items: { $ref: "#/components/schemas/TaskSubtreeTransferExpectedTask" }
59814
+ },
59815
+ contained_plan_ids: {
59816
+ type: "array",
59817
+ items: { type: "string", format: "uuid" }
59818
+ },
59819
+ shared_plan_ids: {
59820
+ type: "array",
59821
+ items: { type: "string", format: "uuid" }
59822
+ },
59823
+ complete: { type: "boolean", enum: [true] }
59824
+ }
59825
+ };
59826
+ taskSubtreeTransferApplyRequestSchema = {
59827
+ type: "object",
59828
+ additionalProperties: false,
59829
+ required: [
59830
+ "version",
59831
+ "operation_id",
59832
+ "step_id",
59833
+ "idempotency_key",
59834
+ "precondition_digest",
59835
+ "source_project_id",
59836
+ "destination_project_id",
59837
+ "destination_task_list_id",
59838
+ "root_task_id",
59839
+ "destination_parent_id",
59840
+ "expected_root_parent_id",
59841
+ "source_population_digest",
59842
+ "expected_tasks",
59843
+ "shared_plan_splits"
59844
+ ],
59845
+ properties: {
59846
+ ...taskSubtreeTransferInspectRequestSchema.properties,
59847
+ version: { type: "integer", enum: [1] },
59848
+ operation_id: { type: "string", minLength: 1, maxLength: 200 },
59849
+ step_id: { type: "string", minLength: 1, maxLength: 200 },
59850
+ idempotency_key: { type: "string", minLength: 1, maxLength: 240 },
59851
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
59852
+ expected_root_parent_id: { type: "string", format: "uuid", nullable: true },
59853
+ source_population_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
59854
+ expected_tasks: {
59855
+ type: "array",
59856
+ minItems: 1,
59857
+ items: { $ref: "#/components/schemas/TaskSubtreeTransferExpectedTask" }
59858
+ },
59859
+ shared_plan_splits: {
59860
+ type: "array",
59861
+ items: { $ref: "#/components/schemas/TaskSubtreeTransferSharedPlanSplit" }
59862
+ }
59863
+ }
59864
+ };
59865
+ taskSubtreeTransferTaskImageSchema = {
59866
+ type: "object",
59867
+ additionalProperties: false,
59868
+ required: [
59869
+ "task_id",
59870
+ "project_id",
59871
+ "parent_id",
59872
+ "plan_id",
59873
+ "task_list_id",
59874
+ "version",
59875
+ "updated_at"
59876
+ ],
59877
+ properties: {
59878
+ task_id: { type: "string", format: "uuid" },
59879
+ project_id: { type: "string", format: "uuid", nullable: true },
59880
+ parent_id: { type: "string", format: "uuid", nullable: true },
59881
+ plan_id: { type: "string", format: "uuid", nullable: true },
59882
+ task_list_id: { type: "string", format: "uuid", nullable: true },
59883
+ version: { type: "integer", minimum: 1 },
59884
+ updated_at: { type: "string", format: "date-time" }
59885
+ }
59886
+ };
59887
+ taskSubtreeTransferPlanImageSchema = {
59888
+ type: "object",
59889
+ additionalProperties: false,
59890
+ required: ["plan_id", "project_id", "task_list_id", "updated_at"],
59891
+ properties: {
59892
+ plan_id: { type: "string", format: "uuid" },
59893
+ project_id: { type: "string", format: "uuid", nullable: true },
59894
+ task_list_id: { type: "string", format: "uuid", nullable: true },
59895
+ updated_at: { type: "string", format: "date-time" }
59896
+ }
59897
+ };
59898
+ taskSubtreeTransferImageSchema = {
59899
+ type: "object",
59900
+ additionalProperties: false,
59901
+ required: ["tasks", "plans"],
59902
+ properties: {
59903
+ tasks: {
59904
+ type: "array",
59905
+ items: { $ref: "#/components/schemas/TaskSubtreeTransferTaskImage" }
59906
+ },
59907
+ plans: {
59908
+ type: "array",
59909
+ items: { $ref: "#/components/schemas/TaskSubtreeTransferPlanImage" }
59910
+ }
59911
+ }
59912
+ };
59913
+ taskSubtreeTransferReceiptSchema = {
59914
+ type: "object",
59915
+ additionalProperties: false,
59916
+ required: [
59917
+ "receipt_id",
59918
+ "authority",
59919
+ "route",
59920
+ "schema_version",
59921
+ "kind",
59922
+ "operation_id",
59923
+ "step_id",
59924
+ "idempotency_key",
59925
+ "request_digest",
59926
+ "precondition_digest",
59927
+ "result_digest",
59928
+ "apply_receipt_id",
59929
+ "source_project_id",
59930
+ "destination_project_id",
59931
+ "destination_task_list_id",
59932
+ "root_task_id",
59933
+ "source_population_digest",
59934
+ "prior_image",
59935
+ "post_image",
59936
+ "shared_plan_splits",
59937
+ "created_at"
59938
+ ],
59939
+ properties: {
59940
+ receipt_id: { type: "string", format: "uuid" },
59941
+ authority: { type: "string", enum: ["todos"] },
59942
+ route: { type: "string", enum: ["todos.task-subtree-transfer.v1"] },
59943
+ schema_version: { type: "integer", enum: [1] },
59944
+ kind: { type: "string", enum: ["apply", "rollback"] },
59945
+ operation_id: { type: "string" },
59946
+ step_id: { type: "string" },
59947
+ idempotency_key: { type: "string" },
59948
+ request_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
59949
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
59950
+ result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
59951
+ apply_receipt_id: { type: "string", format: "uuid", nullable: true },
59952
+ source_project_id: { type: "string", format: "uuid" },
59953
+ destination_project_id: { type: "string", format: "uuid" },
59954
+ destination_task_list_id: { type: "string", format: "uuid" },
59955
+ root_task_id: { type: "string", format: "uuid" },
59956
+ source_population_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
59957
+ prior_image: { $ref: "#/components/schemas/TaskSubtreeTransferImage" },
59958
+ post_image: { $ref: "#/components/schemas/TaskSubtreeTransferImage" },
59959
+ shared_plan_splits: {
59960
+ type: "array",
59961
+ items: { $ref: "#/components/schemas/TaskSubtreeTransferSharedPlanSplit" }
59962
+ },
59963
+ created_at: { type: "string", format: "date-time" }
59964
+ }
59965
+ };
59966
+ taskSubtreeTransferResultSchema = {
59967
+ type: "object",
59968
+ additionalProperties: false,
59969
+ required: ["duplicate", "receipt", "moved_task_ids", "moved_plan_ids", "complete"],
59970
+ properties: {
59971
+ duplicate: { type: "boolean" },
59972
+ receipt: { $ref: "#/components/schemas/TaskSubtreeTransferReceipt" },
59973
+ moved_task_ids: { type: "array", items: { type: "string", format: "uuid" } },
59974
+ moved_plan_ids: { type: "array", items: { type: "string", format: "uuid" } },
59975
+ complete: { type: "boolean", enum: [true] }
59976
+ }
59977
+ };
59978
+ taskSubtreeTransferCapabilitySchema = {
59979
+ type: "object",
59980
+ additionalProperties: false,
59981
+ required: [
59982
+ "authority",
59983
+ "route",
59984
+ "schema_version",
59985
+ "tenant_id",
59986
+ "backend",
59987
+ "exact_descendant_closure",
59988
+ "complete_source_population_digest",
59989
+ "per_task_version_cas",
59990
+ "explicit_shared_plan_splits",
59991
+ "atomic_apply",
59992
+ "immutable_forward_inverse_receipts",
59993
+ "prior_image_receipts",
59994
+ "cas_protected_rollback",
59995
+ "preserves_descendant_parent_ids",
59996
+ "preserves_task_and_relation_identities"
59997
+ ],
59998
+ properties: {
59999
+ authority: { type: "string", enum: ["todos"] },
60000
+ route: { type: "string", enum: ["todos.task-subtree-transfer.v1"] },
60001
+ schema_version: { type: "integer", enum: [1] },
60002
+ tenant_id: { type: "string", minLength: 1, maxLength: 200 },
60003
+ backend: { type: "string", enum: ["sqlite", "postgresql", "http"] },
60004
+ exact_descendant_closure: { type: "boolean", enum: [true] },
60005
+ complete_source_population_digest: { type: "boolean", enum: [true] },
60006
+ per_task_version_cas: { type: "boolean", enum: [true] },
60007
+ explicit_shared_plan_splits: { type: "boolean", enum: [true] },
60008
+ atomic_apply: { type: "boolean", enum: [true] },
60009
+ immutable_forward_inverse_receipts: { type: "boolean", enum: [true] },
60010
+ prior_image_receipts: { type: "boolean", enum: [true] },
60011
+ cas_protected_rollback: { type: "boolean", enum: [true] },
60012
+ preserves_descendant_parent_ids: { type: "boolean", enum: [true] },
60013
+ preserves_task_and_relation_identities: { type: "boolean", enum: [true] }
60014
+ }
60015
+ };
60016
+ taskSubtreeTransferRollbackRequestSchema = {
60017
+ type: "object",
60018
+ additionalProperties: false,
60019
+ required: ["receipt_id", "operation_id", "step_id", "idempotency_key", "precondition_digest"],
60020
+ properties: {
60021
+ receipt_id: { type: "string", format: "uuid" },
60022
+ operation_id: { type: "string", minLength: 1, maxLength: 200 },
60023
+ step_id: { type: "string", minLength: 1, maxLength: 200 },
60024
+ idempotency_key: { type: "string", minLength: 1, maxLength: 240 },
60025
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" }
60026
+ }
60027
+ };
60028
+ taskSubtreeTransferReadExactRequestSchema = {
60029
+ type: "object",
60030
+ additionalProperties: false,
60031
+ required: ["receipt_id"],
60032
+ properties: {
60033
+ receipt_id: { type: "string", format: "uuid" }
60034
+ }
60035
+ };
58285
60036
  taskManifestCompensateRequestSchema = {
58286
60037
  type: "object",
58287
60038
  additionalProperties: false,
@@ -58960,8 +60711,8 @@ var exports_pr_groups = {};
58960
60711
  __export(exports_pr_groups, {
58961
60712
  handlePrGroupHttpRequest: () => handlePrGroupHttpRequest
58962
60713
  });
58963
- function json4(body2, status2 = 200) {
58964
- return new Response(JSON.stringify(body2), { status: status2, headers: JSON_HEADERS3 });
60714
+ function json5(body2, status3 = 200) {
60715
+ return new Response(JSON.stringify(body2), { status: status3, headers: JSON_HEADERS4 });
58965
60716
  }
58966
60717
  function errorStatus2(error) {
58967
60718
  switch (error.code) {
@@ -58995,26 +60746,26 @@ async function handlePrGroupHttpRequest(req, url, ledger, basePath, principal) {
58995
60746
  const method = req.method.toUpperCase();
58996
60747
  try {
58997
60748
  if (!groupId && method === "POST" && action === undefined) {
58998
- return json4({ error: "unknown PR-group route", code: "PR_GROUP_NOT_FOUND" }, 404);
60749
+ return json5({ error: "unknown PR-group route", code: "PR_GROUP_NOT_FOUND" }, 404);
58999
60750
  }
59000
60751
  if (groupId === "admit" && !action) {
59001
60752
  if (method !== "POST")
59002
- return json4({ error: "method not allowed" }, 405);
60753
+ return json5({ error: "method not allowed" }, 405);
59003
60754
  const body2 = await readJson2(req);
59004
60755
  if (!body2)
59005
- return json4({ error: "invalid JSON body", code: "PR_GROUP_INVALID_INPUT" }, 400);
59006
- return json4(await ledger.admit(body2), 201);
60756
+ return json5({ error: "invalid JSON body", code: "PR_GROUP_INVALID_INPUT" }, 400);
60757
+ return json5(await ledger.admit(body2), 201);
59007
60758
  }
59008
60759
  if (!groupId)
59009
- return json4({ error: "PR group id is required", code: "PR_GROUP_INVALID_INPUT" }, 400);
60760
+ return json5({ error: "PR group id is required", code: "PR_GROUP_INVALID_INPUT" }, 400);
59010
60761
  if (!action && method === "GET") {
59011
- return json4({ view: await ledger.get(groupId) });
60762
+ return json5({ view: await ledger.get(groupId) });
59012
60763
  }
59013
60764
  if (action === "events") {
59014
60765
  if (method === "GET") {
59015
60766
  const limit = url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : undefined;
59016
60767
  const afterSequence = url.searchParams.has("after_sequence") ? Number(url.searchParams.get("after_sequence")) : undefined;
59017
- return json4({
60768
+ return json5({
59018
60769
  history: await ledger.events(groupId, {
59019
60770
  ...limit !== undefined ? { limit } : {},
59020
60771
  ...afterSequence !== undefined ? { after_sequence: afterSequence } : {}
@@ -59024,72 +60775,49 @@ async function handlePrGroupHttpRequest(req, url, ledger, basePath, principal) {
59024
60775
  if (method === "POST") {
59025
60776
  const body2 = await readJson2(req);
59026
60777
  if (!body2)
59027
- return json4({ error: "invalid JSON body", code: "PR_GROUP_INVALID_INPUT" }, 400);
59028
- return json4(await ledger.append({
60778
+ return json5({ error: "invalid JSON body", code: "PR_GROUP_INVALID_INPUT" }, 400);
60779
+ return json5(await ledger.append({
59029
60780
  ...body2,
59030
60781
  group_id: groupId,
59031
60782
  authenticated_actor_id: principal?.actor_id ?? undefined,
59032
60783
  authenticated_actor_run_id: principal?.actor_run_id ?? undefined
59033
60784
  }), 201);
59034
60785
  }
59035
- return json4({ error: "method not allowed" }, 405);
60786
+ return json5({ error: "method not allowed" }, 405);
59036
60787
  }
59037
60788
  if (action === "recover") {
59038
60789
  if (method !== "POST")
59039
- return json4({ error: "method not allowed" }, 405);
60790
+ return json5({ error: "method not allowed" }, 405);
59040
60791
  const body2 = await readJson2(req);
59041
60792
  if (!body2)
59042
- return json4({ error: "invalid JSON body", code: "PR_GROUP_INVALID_INPUT" }, 400);
59043
- return json4(await ledger.recover({
60793
+ return json5({ error: "invalid JSON body", code: "PR_GROUP_INVALID_INPUT" }, 400);
60794
+ return json5(await ledger.recover({
59044
60795
  ...body2,
59045
60796
  group_id: groupId
59046
60797
  }), 201);
59047
60798
  }
59048
- return json4({ error: "unknown PR-group route", code: "PR_GROUP_NOT_FOUND" }, 404);
60799
+ return json5({ error: "unknown PR-group route", code: "PR_GROUP_NOT_FOUND" }, 404);
59049
60800
  } catch (cause) {
59050
60801
  if (cause instanceof PrGroupLedgerError) {
59051
- return json4({
60802
+ return json5({
59052
60803
  error: cause.message,
59053
60804
  code: cause.code,
59054
60805
  details: cause.details,
59055
60806
  authoritative: true
59056
60807
  }, errorStatus2(cause));
59057
60808
  }
59058
- return json4({
60809
+ return json5({
59059
60810
  error: cause instanceof Error ? cause.message : "internal PR-group error",
59060
60811
  code: "PR_GROUP_ATOMICITY_UNAVAILABLE"
59061
60812
  }, 500);
59062
60813
  }
59063
60814
  }
59064
- var JSON_HEADERS3;
60815
+ var JSON_HEADERS4;
59065
60816
  var init_pr_groups = __esm(() => {
59066
60817
  init_types3();
59067
- JSON_HEADERS3 = { "Content-Type": "application/json" };
60818
+ JSON_HEADERS4 = { "Content-Type": "application/json" };
59068
60819
  });
59069
60820
 
59070
- // src/lib/comment-cursor.ts
59071
- function encodeCommentCursor(comment2) {
59072
- return Buffer.from(JSON.stringify({ created_at: comment2.created_at, id: comment2.id }), "utf8").toString("base64url");
59073
- }
59074
- function decodeCommentCursor(value) {
59075
- if (value.length > MAX_COMMENT_CURSOR_LENGTH)
59076
- throw new Error("invalid comment cursor");
59077
- let parsed;
59078
- try {
59079
- parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
59080
- } catch {
59081
- throw new Error("invalid comment cursor");
59082
- }
59083
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
59084
- throw new Error("invalid comment cursor");
59085
- const cursor = parsed;
59086
- if (typeof cursor["created_at"] !== "string" || cursor["created_at"].length > 64 || !Number.isFinite(Date.parse(cursor["created_at"])) || typeof cursor["id"] !== "string" || !cursor["id"] || cursor["id"].length > 256) {
59087
- throw new Error("invalid comment cursor");
59088
- }
59089
- return { created_at: cursor["created_at"], id: cursor["id"] };
59090
- }
59091
- var MAX_COMMENT_CURSOR_LENGTH = 1024;
59092
-
59093
60821
  // src/lib/project-task-list-ensure.ts
59094
60822
  import { createHash as createHash17 } from "crypto";
59095
60823
  function canonicalJson2(value) {
@@ -59099,11 +60827,11 @@ function canonicalJson2(value) {
59099
60827
  return `[${value.map(canonicalJson2).join(",")}]`;
59100
60828
  return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key2, item]) => `${JSON.stringify(key2)}:${canonicalJson2(item)}`).join(",")}}`;
59101
60829
  }
59102
- function digest2(value) {
60830
+ function digest3(value) {
59103
60831
  return createHash17("sha256").update(canonicalJson2(value)).digest("hex");
59104
60832
  }
59105
60833
  function deriveIdempotencyKey(projectId, slug) {
59106
- return `ptlk_${digest2({ project_id: projectId, slug }).slice(0, 48)}`;
60834
+ return `ptlk_${digest3({ project_id: projectId, slug }).slice(0, 48)}`;
59107
60835
  }
59108
60836
  function normalizeIdempotencyKey(value, projectId, slug) {
59109
60837
  const key2 = value?.trim() || deriveIdempotencyKey(projectId, slug);
@@ -59113,12 +60841,12 @@ function normalizeIdempotencyKey(value, projectId, slug) {
59113
60841
  return key2;
59114
60842
  }
59115
60843
  function receiptId2(projectId, slug, idempotencyKey2) {
59116
- return `ptlr_${digest2({ project_id: projectId, slug, idempotency_key: idempotencyKey2 }).slice(0, 48)}`;
60844
+ return `ptlr_${digest3({ project_id: projectId, slug, idempotency_key: idempotencyKey2 }).slice(0, 48)}`;
59117
60845
  }
59118
60846
  function semanticListDigest(list) {
59119
60847
  const metadata = { ...list.metadata ?? {} };
59120
60848
  delete metadata[RECEIPT_METADATA_KEY];
59121
- return digest2({
60849
+ return digest3({
59122
60850
  project_id: list.project_id,
59123
60851
  slug: list.slug,
59124
60852
  name: list.name,
@@ -59147,7 +60875,7 @@ function receiptFor(store, project, list, idempotencyKey2) {
59147
60875
  }
59148
60876
  return {
59149
60877
  schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
59150
- receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest2({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
60878
+ receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest3({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
59151
60879
  idempotency_key: owned ? marker.idempotency_key : idempotencyKey2,
59152
60880
  project_id: project.id,
59153
60881
  task_list_id: list.id,
@@ -59326,7 +61054,7 @@ async function rollbackProjectTaskListEnsure(store, projectId, options) {
59326
61054
  project_id: project.id,
59327
61055
  task_list_id: list.id,
59328
61056
  accepted_receipt_id: options.receipt_id,
59329
- rollback_receipt_id: `ptlr_inverse_${digest2({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
61057
+ rollback_receipt_id: `ptlr_inverse_${digest3({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
59330
61058
  removed_at: new Date().toISOString()
59331
61059
  };
59332
61060
  }
@@ -59421,11 +61149,11 @@ __export(exports_v1, {
59421
61149
  handleV1Request: () => handleV1Request,
59422
61150
  countSnapshotRecords: () => countSnapshotRecords
59423
61151
  });
59424
- function json5(body2, status2 = 200) {
59425
- return new Response(JSON.stringify(body2), { status: status2, headers: JSON_HEADERS4 });
61152
+ function json6(body2, status3 = 200) {
61153
+ return new Response(JSON.stringify(body2), { status: status3, headers: JSON_HEADERS5 });
59426
61154
  }
59427
- function error(status2, message, extra) {
59428
- return json5({ error: message, ...extra ?? {} }, status2);
61155
+ function error(status3, message, extra) {
61156
+ return json6({ error: message, ...extra ?? {} }, status3);
59429
61157
  }
59430
61158
  function enumQueryParam(url, name, vocabulary) {
59431
61159
  const raw = url.searchParams.get(name);
@@ -59764,6 +61492,9 @@ function contextFromPrincipal(principal, body2) {
59764
61492
  function redactComment3(comment2) {
59765
61493
  return { ...comment2, content: redactEvidenceText(comment2.content) };
59766
61494
  }
61495
+ function redactPlanComment2(comment2) {
61496
+ return { ...comment2, content: redactEvidenceText(comment2.content) };
61497
+ }
59767
61498
  function normalizeImportSnapshot(raw) {
59768
61499
  const body2 = raw && typeof raw === "object" ? raw : {};
59769
61500
  const arr = (v) => Array.isArray(v) ? v : [];
@@ -59872,6 +61603,9 @@ async function handleV1Request(req, url, dependencies = {}) {
59872
61603
  if (path === "/v1/task-manifest" || path.startsWith("/v1/task-manifest/")) {
59873
61604
  return handleTodosTaskManifestHttpRequest(req, url, (dependencies.getTaskManifestAuthority ?? getCloudTaskManifestAuthority)());
59874
61605
  }
61606
+ if (path === "/v1/task-subtree-transfer" || path.startsWith("/v1/task-subtree-transfer/")) {
61607
+ return handleTodosTaskSubtreeTransferHttpRequest(req, url, (dependencies.getTaskSubtreeTransferAuthority ?? getCloudTaskSubtreeTransferAuthority)());
61608
+ }
59875
61609
  const store = (dependencies.getStorageAdapter ?? getCloudStorageAdapter)();
59876
61610
  const segments = path.split("/").filter(Boolean);
59877
61611
  const resource = segments[1];
@@ -59893,7 +61627,7 @@ async function handleV1Request(req, url, dependencies = {}) {
59893
61627
  const presentSet = new Set(found.map((t) => t.id));
59894
61628
  const present = ids2.filter((i) => presentSet.has(i));
59895
61629
  const missing = ids2.filter((i) => !presentSet.has(i));
59896
- return json5({
61630
+ return json6({
59897
61631
  requested: ids2.length,
59898
61632
  present_count: present.length,
59899
61633
  missing_count: missing.length,
@@ -59943,11 +61677,11 @@ async function handleV1Request(req, url, dependencies = {}) {
59943
61677
  }
59944
61678
  if (!existing) {
59945
61679
  const task3 = await store.tasks.create({ ...fields, title: body2.title }, contextFromPrincipal(principal, body2));
59946
- return json5({ task: task3, created: true }, 201);
61680
+ return json6({ task: task3, created: true }, 201);
59947
61681
  }
59948
61682
  try {
59949
61683
  const task3 = await store.tasks.update(existing.id, { ...fields, version: existing.version }, contextFromPrincipal(principal, body2));
59950
- return json5({ task: task3, created: false });
61684
+ return json6({ task: task3, created: false });
59951
61685
  } catch (e) {
59952
61686
  const msg = e.message || "";
59953
61687
  if (msg.includes("version conflict"))
@@ -59995,7 +61729,7 @@ async function handleV1Request(req, url, dependencies = {}) {
59995
61729
  const tasks = await store.tasks.list(filter);
59996
61730
  const { limit: _l, offset: _o, ...countFilter } = filter;
59997
61731
  const total = await store.tasks.count(countFilter);
59998
- return json5({ tasks, count: tasks.length, total });
61732
+ return json6({ tasks, count: tasks.length, total });
59999
61733
  }
60000
61734
  if (method === "POST") {
60001
61735
  const body2 = await readJson3(req);
@@ -60020,7 +61754,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60020
61754
  if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null) || (persisted.plan_id ?? null) !== (body2.plan_id ?? null) || body2.created_by !== undefined && persisted.created_by !== body2.created_by) {
60021
61755
  return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id, parent_id, plan_id, and explicit created_by", { code: "TASK_CREATE_PERSISTENCE_UNVERIFIED" });
60022
61756
  }
60023
- return json5({ task: persisted }, 201);
61757
+ return json6({ task: persisted }, 201);
60024
61758
  }
60025
61759
  return error(405, `method ${method} not allowed on /v1/tasks`);
60026
61760
  }
@@ -60062,7 +61796,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60062
61796
  new_holder: body3.new_holder,
60063
61797
  reason: body3.reason
60064
61798
  }, contextFromPrincipal(principal));
60065
- return json5({ receipt });
61799
+ return json6({ receipt });
60066
61800
  }
60067
61801
  if (action === "comments") {
60068
61802
  if (method === "GET") {
@@ -60076,7 +61810,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60076
61810
  if (legacyPage.length > LEGACY_COMMENT_RESPONSE_LIMIT) {
60077
61811
  return error(426, "task has too many comments for this client; upgrade @hasna/todos to use cursor pagination");
60078
61812
  }
60079
- return json5({
61813
+ return json6({
60080
61814
  comments: legacyPage,
60081
61815
  count: legacyPage.length,
60082
61816
  has_more: false,
@@ -60101,7 +61835,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60101
61835
  const page = (await store.audit.getCommentsPage(id, { limit: limit + 1, ...before ? { before } : {} }, contextFromPrincipal(principal))).map(redactComment3);
60102
61836
  const hasMore = page.length > limit;
60103
61837
  const comments = hasMore ? page.slice(1) : page;
60104
- return json5({
61838
+ return json6({
60105
61839
  comments,
60106
61840
  count: comments.length,
60107
61841
  has_more: hasMore,
@@ -60124,7 +61858,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60124
61858
  type: body3.type,
60125
61859
  progress_pct: body3.progress_pct
60126
61860
  }, contextFromPrincipal(principal, body3));
60127
- return json5({ comment: redactComment3(comment2) }, 201);
61861
+ return json6({ comment: redactComment3(comment2) }, 201);
60128
61862
  }
60129
61863
  return error(405, `method ${method} not allowed on /v1/tasks/:id/comments`);
60130
61864
  }
@@ -60134,7 +61868,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60134
61868
  if (!await store.tasks.get(id))
60135
61869
  return error(404, "task not found");
60136
61870
  const history = await store.audit.getTaskHistory(id);
60137
- return json5({ history, count: history.length });
61871
+ return json6({ history, count: history.length });
60138
61872
  }
60139
61873
  if (action === "lock" || action === "unlock") {
60140
61874
  if (method !== "POST")
@@ -60146,7 +61880,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60146
61880
  if (typeof store.tasks.lock !== "function")
60147
61881
  return error(501, "task locking is not supported by this storage backend");
60148
61882
  const agentId3 = body3.agent_id || principal.agent || "todos-serve";
60149
- return json5({ result: await store.tasks.lock(id, agentId3) });
61883
+ return json6({ result: await store.tasks.lock(id, agentId3) });
60150
61884
  }
60151
61885
  if (typeof store.tasks.unlock !== "function")
60152
61886
  return error(501, "task unlocking is not supported by this storage backend");
@@ -60154,7 +61888,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60154
61888
  if (!principal.scopes.includes("todos:*"))
60155
61889
  return error(403, "force unlock requires todos:* scope");
60156
61890
  const released2 = await store.tasks.unlock(id);
60157
- return json5({ success: released2 });
61891
+ return json6({ success: released2 });
60158
61892
  }
60159
61893
  if (body3.agent_id && body3.agent_id !== principal.agent && !principal.scopes.includes("todos:*")) {
60160
61894
  return error(403, "unlock agent_id must match the authenticated agent");
@@ -60163,7 +61897,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60163
61897
  if (!agentId2)
60164
61898
  return error(403, "unlock requires an agent-bound key or force=true");
60165
61899
  const released = await store.tasks.unlock(id, agentId2);
60166
- return json5({ success: released });
61900
+ return json6({ success: released });
60167
61901
  }
60168
61902
  if (action === "dependencies") {
60169
61903
  if (!store.dependencies)
@@ -60172,7 +61906,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60172
61906
  if (!await store.tasks.get(id))
60173
61907
  return error(404, "task not found");
60174
61908
  const edges = await store.dependencies.list(id);
60175
- return json5(edges);
61909
+ return json6(edges);
60176
61910
  }
60177
61911
  if (method === "POST") {
60178
61912
  const body3 = await readJson3(req) ?? {};
@@ -60181,7 +61915,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60181
61915
  }
60182
61916
  try {
60183
61917
  const dependency2 = await store.dependencies.add(id, body3.depends_on, contextFromPrincipal(principal));
60184
- return json5({ dependency: dependency2 }, 201);
61918
+ return json6({ dependency: dependency2 }, 201);
60185
61919
  } catch (e) {
60186
61920
  const msg = e.message || "";
60187
61921
  if (msg.includes("not found"))
@@ -60195,7 +61929,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60195
61929
  if (!subId)
60196
61930
  return error(400, "dependency target id is required (/v1/tasks/:id/dependencies/:dep)");
60197
61931
  const removed = await store.dependencies.remove(id, subId);
60198
- return json5({ removed });
61932
+ return json6({ removed });
60199
61933
  }
60200
61934
  return error(405, `method ${method} not allowed on /v1/tasks/:id/dependencies`);
60201
61935
  }
@@ -60206,7 +61940,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60206
61940
  if (!await store.tasks.get(id))
60207
61941
  return error(404, "task not found");
60208
61942
  const verifications = await store.verifications.list(id);
60209
- return json5({ verifications, count: verifications.length });
61943
+ return json6({ verifications, count: verifications.length });
60210
61944
  }
60211
61945
  if (method === "POST") {
60212
61946
  const body3 = await readJson3(req) ?? {};
@@ -60222,7 +61956,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60222
61956
  artifact_path: body3.artifact_path,
60223
61957
  agent_id: body3.agent_id
60224
61958
  }, contextFromPrincipal(principal, body3));
60225
- return json5({ verification: verification2 }, 201);
61959
+ return json6({ verification: verification2 }, 201);
60226
61960
  } catch (e) {
60227
61961
  const msg = e.message || "";
60228
61962
  if (msg.includes("not found"))
@@ -60239,7 +61973,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60239
61973
  if (!await store.tasks.get(id))
60240
61974
  return error(404, "task not found");
60241
61975
  const commits = await store.commits.list(id);
60242
- return json5({ commits, count: commits.length });
61976
+ return json6({ commits, count: commits.length });
60243
61977
  }
60244
61978
  if (method === "POST") {
60245
61979
  const body3 = await readJson3(req) ?? {};
@@ -60253,7 +61987,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60253
61987
  author: body3.author,
60254
61988
  files_changed: Array.isArray(body3.files_changed) ? body3.files_changed : undefined
60255
61989
  }, contextFromPrincipal(principal));
60256
- return json5({ commit }, 201);
61990
+ return json6({ commit }, 201);
60257
61991
  } catch (e) {
60258
61992
  const msg = e.message || "";
60259
61993
  if (msg.includes("not found"))
@@ -60270,7 +62004,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60270
62004
  if (!await store.tasks.get(id))
60271
62005
  return error(404, "task not found");
60272
62006
  const refs = await store.gitRefs.list(id);
60273
- return json5({ refs, count: refs.length });
62007
+ return json6({ refs, count: refs.length });
60274
62008
  }
60275
62009
  if (method === "POST") {
60276
62010
  const body3 = await readJson3(req) ?? {};
@@ -60286,7 +62020,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60286
62020
  provider: body3.provider,
60287
62021
  metadata: body3.metadata
60288
62022
  }, contextFromPrincipal(principal));
60289
- return json5({ ref }, 201);
62023
+ return json6({ ref }, 201);
60290
62024
  } catch (e) {
60291
62025
  const msg = e.message || "";
60292
62026
  if (msg.includes("not found"))
@@ -60302,13 +62036,13 @@ async function handleV1Request(req, url, dependencies = {}) {
60302
62036
  const body2 = actionJson.value && typeof actionJson.value === "object" && !Array.isArray(actionJson.value) ? actionJson.value : {};
60303
62037
  const agentId = typeof body2.agent_id === "string" ? body2.agent_id : principal.agent || "todos-serve";
60304
62038
  if (action === "start" && method === "POST") {
60305
- return json5({ task: await store.tasks.start(id, agentId) });
62039
+ return json6({ task: await store.tasks.start(id, agentId) });
60306
62040
  }
60307
62041
  if (action === "complete" && method === "POST") {
60308
62042
  const parsed = validateTaskCompletion(actionJson.value);
60309
62043
  if (!parsed.ok)
60310
62044
  return error(400, parsed.message);
60311
- return json5({
62045
+ return json6({
60312
62046
  task: await store.tasks.complete(id, parsed.agentId || principal.agent || "todos-serve", parsed.options, contextFromPrincipal(principal, body2))
60313
62047
  });
60314
62048
  }
@@ -60316,12 +62050,12 @@ async function handleV1Request(req, url, dependencies = {}) {
60316
62050
  const parsed = validateTaskFailure(actionJson.value);
60317
62051
  if (!parsed.ok)
60318
62052
  return error(400, parsed.message);
60319
- return json5({
62053
+ return json6({
60320
62054
  result: await store.tasks.fail(id, parsed.agentId || principal.agent || "todos-serve", parsed.reason, { retry: parsed.retry }, contextFromPrincipal(principal, body2))
60321
62055
  });
60322
62056
  }
60323
62057
  if (action === "claim" && method === "POST") {
60324
- return json5({ task: await store.tasks.claimNext(agentId, {}) });
62058
+ return json6({ task: await store.tasks.claimNext(agentId, {}) });
60325
62059
  }
60326
62060
  return error(404, `unknown task action: ${action}`);
60327
62061
  }
@@ -60344,7 +62078,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60344
62078
  throw e;
60345
62079
  }
60346
62080
  }
60347
- return task3 ? json5({ task: task3 }) : error(404, "task not found");
62081
+ return task3 ? json6({ task: task3 }) : error(404, "task not found");
60348
62082
  }
60349
62083
  if (method === "PATCH" || method === "PUT") {
60350
62084
  const rawBody = await readJson3(req);
@@ -60363,7 +62097,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60363
62097
  };
60364
62098
  try {
60365
62099
  const task3 = await store.tasks.update(id, patch);
60366
- return task3 ? json5({ task: task3 }) : error(404, "task not found");
62100
+ return task3 ? json6({ task: task3 }) : error(404, "task not found");
60367
62101
  } catch (e) {
60368
62102
  const msg = e.message || "";
60369
62103
  if (msg.includes("version conflict"))
@@ -60373,7 +62107,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60373
62107
  }
60374
62108
  if (method === "DELETE") {
60375
62109
  await store.tasks.delete(id, contextFromPrincipal(principal));
60376
- return json5({ deleted: true, id });
62110
+ return json6({ deleted: true, id });
60377
62111
  }
60378
62112
  return error(405, `method ${method} not allowed on /v1/tasks/:id`);
60379
62113
  }
@@ -60381,7 +62115,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60381
62115
  if (!id) {
60382
62116
  if (method === "GET") {
60383
62117
  const projects = await store.projects.list();
60384
- return json5({ projects, count: projects.length });
62118
+ return json6({ projects, count: projects.length });
60385
62119
  }
60386
62120
  if (method === "POST") {
60387
62121
  const body2 = await readJson3(req);
@@ -60391,13 +62125,13 @@ async function handleV1Request(req, url, dependencies = {}) {
60391
62125
  if (!validated.ok)
60392
62126
  return error(400, validated.message);
60393
62127
  const project = await store.projects.create(validated.input, contextFromPrincipal(principal));
60394
- return json5({ project }, 201);
62128
+ return json6({ project }, 201);
60395
62129
  }
60396
62130
  return error(405, `method ${method} not allowed on /v1/projects`);
60397
62131
  }
60398
62132
  if (action === "task-list" && subId === "ensure") {
60399
62133
  if (method === "GET") {
60400
- return json5(await planProjectTaskListEnsure(store, id));
62134
+ return json6(await planProjectTaskListEnsure(store, id));
60401
62135
  }
60402
62136
  if (method !== "POST") {
60403
62137
  return error(405, `method ${method} not allowed on /v1/projects/:id/task-list/ensure`);
@@ -60418,7 +62152,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60418
62152
  expected_project_revision: body2.expected_project_revision,
60419
62153
  ...typeof body2.idempotency_key === "string" ? { idempotency_key: body2.idempotency_key } : {}
60420
62154
  });
60421
- return json5(result, result.action === "created" ? 201 : 200);
62155
+ return json6(result, result.action === "created" ? 201 : 200);
60422
62156
  }
60423
62157
  if (action === "task-list" && subId === "rollback") {
60424
62158
  if (method !== "POST") {
@@ -60436,7 +62170,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60436
62170
  if (typeof body2.expected_task_list_revision !== "string" || !body2.expected_task_list_revision.trim()) {
60437
62171
  return error(400, "expected_task_list_revision must be a non-empty string from the accepted receipt");
60438
62172
  }
60439
- return json5(await rollbackProjectTaskListEnsure(store, id, {
62173
+ return json6(await rollbackProjectTaskListEnsure(store, id, {
60440
62174
  receipt_id: body2.receipt_id,
60441
62175
  expected_task_list_revision: body2.expected_task_list_revision
60442
62176
  }));
@@ -60454,11 +62188,11 @@ async function handleV1Request(req, url, dependencies = {}) {
60454
62188
  const unknownField = Object.keys(body2).find((key2) => !["new_slug", "name"].includes(key2));
60455
62189
  if (unknownField)
60456
62190
  return error(400, `unknown project rename field: ${unknownField}`);
60457
- return json5(await store.projects.rename(id, body2, contextFromPrincipal(principal)));
62191
+ return json6(await store.projects.rename(id, body2, contextFromPrincipal(principal)));
60458
62192
  }
60459
62193
  if (method === "GET") {
60460
62194
  const project = await store.projects.get(id);
60461
- return project ? json5({ project }) : error(404, "project not found");
62195
+ return project ? json6({ project }) : error(404, "project not found");
60462
62196
  }
60463
62197
  if (method === "PATCH" || method === "PUT") {
60464
62198
  const body2 = await readJson3(req);
@@ -60470,18 +62204,18 @@ async function handleV1Request(req, url, dependencies = {}) {
60470
62204
  if (!await store.projects.get(id))
60471
62205
  return error(404, "project not found");
60472
62206
  const project = await store.projects.update(id, validated.patch);
60473
- return json5({ project });
62207
+ return json6({ project });
60474
62208
  }
60475
62209
  if (method === "DELETE") {
60476
62210
  await store.projects.delete(id, contextFromPrincipal(principal));
60477
- return json5({ deleted: true, id });
62211
+ return json6({ deleted: true, id });
60478
62212
  }
60479
62213
  return error(405, `method ${method} not allowed on /v1/projects/:id`);
60480
62214
  }
60481
62215
  if (resource === "plans") {
60482
62216
  if (!id && method === "GET") {
60483
62217
  const plans = await store.plans.list(url.searchParams.get("project_id") ?? undefined);
60484
- return json5({ plans, count: plans.length });
62218
+ return json6({ plans, count: plans.length });
60485
62219
  }
60486
62220
  if (!id && method === "POST") {
60487
62221
  const body2 = await readJson3(req);
@@ -60499,14 +62233,14 @@ async function handleV1Request(req, url, dependencies = {}) {
60499
62233
  }
60500
62234
  }
60501
62235
  const plan = await store.plans.create(validated.input, contextFromPrincipal(principal, validated.input));
60502
- return json5({ plan }, 201);
62236
+ return json6({ plan }, 201);
60503
62237
  }
60504
62238
  if (id && action === "project-link" && !subId) {
60505
62239
  if (method === "GET") {
60506
62240
  const projectId = url.searchParams.get("project_id");
60507
62241
  if (!projectId?.trim())
60508
62242
  return error(400, "project_id query parameter is required");
60509
- return json5(await planPlanProjectLink(store, id, projectId));
62243
+ return json6(await planPlanProjectLink(store, id, projectId));
60510
62244
  }
60511
62245
  if (method !== "POST")
60512
62246
  return error(405, `method ${method} not allowed on /v1/plans/:id/project-link`);
@@ -60527,7 +62261,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60527
62261
  expected_project_revision: body2.expected_project_revision,
60528
62262
  idempotency_key: body2.idempotency_key
60529
62263
  });
60530
- return json5(result, result.action === "linked" ? 201 : 200);
62264
+ return json6(result, result.action === "linked" ? 201 : 200);
60531
62265
  }
60532
62266
  if (id && action === "project-link" && subId === "rollback") {
60533
62267
  if (method !== "POST")
@@ -60544,14 +62278,69 @@ async function handleV1Request(req, url, dependencies = {}) {
60544
62278
  return error(400, `${field} must be a non-empty string`);
60545
62279
  }
60546
62280
  }
60547
- return json5(await rollbackPlanProjectLink(store, id, body2.project_id, {
62281
+ return json6(await rollbackPlanProjectLink(store, id, body2.project_id, {
60548
62282
  receipt_id: body2.receipt_id,
60549
62283
  expected_plan_revision: body2.expected_plan_revision
60550
62284
  }));
60551
62285
  }
62286
+ if (id && action === "comments") {
62287
+ if (!await store.plans.get(id))
62288
+ return error(404, "plan not found");
62289
+ if (method === "GET") {
62290
+ if (store.plans.getCommentsPage) {
62291
+ const rawLimit = url.searchParams.get("limit");
62292
+ const cursor = url.searchParams.get("cursor");
62293
+ const limit = rawLimit === null ? DEFAULT_COMMENT_PAGE_SIZE : Number(rawLimit);
62294
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_COMMENT_PAGE_SIZE) {
62295
+ return error(400, `limit must be an integer between 1 and ${MAX_COMMENT_PAGE_SIZE}`);
62296
+ }
62297
+ let before;
62298
+ if (cursor) {
62299
+ try {
62300
+ before = decodeCommentCursor(cursor);
62301
+ } catch {
62302
+ return error(400, "invalid comment cursor");
62303
+ }
62304
+ }
62305
+ const page = (await store.plans.getCommentsPage(id, { limit: limit + 1, ...before ? { before } : {} }, contextFromPrincipal(principal))).map(redactPlanComment2);
62306
+ const hasMore = page.length > limit;
62307
+ const comments2 = hasMore ? page.slice(1) : page;
62308
+ return json6({
62309
+ comments: comments2,
62310
+ count: comments2.length,
62311
+ has_more: hasMore,
62312
+ next_cursor: hasMore && comments2[0] ? encodeCommentCursor(comments2[0]) : null
62313
+ });
62314
+ }
62315
+ const comments = (await store.plans.getComments?.(id, contextFromPrincipal(principal)) ?? []).map(redactPlanComment2);
62316
+ return json6({ comments, count: comments.length });
62317
+ }
62318
+ if (method === "POST") {
62319
+ const body2 = await readJson3(req) ?? {};
62320
+ if (typeof body2.content !== "string" || !body2.content.trim()) {
62321
+ return error(400, "content is required");
62322
+ }
62323
+ const target = await store.plans.get(id);
62324
+ if (!target)
62325
+ return error(404, "plan not found");
62326
+ if (!store.plans.addComment) {
62327
+ return error(501, "plan comments are not supported by this storage backend");
62328
+ }
62329
+ const comment2 = await store.plans.addComment({
62330
+ plan_id: id,
62331
+ content: body2.content,
62332
+ agent_id: body2.agent_id ?? principal.agent ?? undefined,
62333
+ session_id: body2.session_id,
62334
+ type: body2.type,
62335
+ progress_pct: body2.progress_pct
62336
+ }, contextFromPrincipal(principal, body2));
62337
+ return json6({ comment: redactPlanComment2(comment2) }, 201);
62338
+ }
62339
+ return error(405, `method ${method} not allowed on /v1/plans/:id/comments`);
62340
+ }
60552
62341
  if (id && method === "GET") {
60553
62342
  const plan = await store.plans.get(id);
60554
- return plan ? json5({ plan }) : error(404, "plan not found");
62343
+ return plan ? json6({ plan }) : error(404, "plan not found");
60555
62344
  }
60556
62345
  if (id && (method === "PATCH" || method === "PUT")) {
60557
62346
  const body2 = await readJson3(req);
@@ -60591,12 +62380,12 @@ async function handleV1Request(req, url, dependencies = {}) {
60591
62380
  }
60592
62381
  }
60593
62382
  const plan = await store.plans.update(id, body2);
60594
- return json5({ plan });
62383
+ return json6({ plan });
60595
62384
  }
60596
62385
  if (id && method === "DELETE") {
60597
62386
  if (!await store.plans.delete(id, contextFromPrincipal(principal)))
60598
62387
  return error(404, "plan not found");
60599
- return json5({ deleted: true, id });
62388
+ return json6({ deleted: true, id });
60600
62389
  }
60601
62390
  if (id)
60602
62391
  return error(405, `method ${method} not allowed on /v1/plans/:id`);
@@ -60605,7 +62394,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60605
62394
  if (!id && method === "GET") {
60606
62395
  const projectId = url.searchParams.get("project_id");
60607
62396
  const templates = (await store.templates.list()).filter((template) => projectId === null || template.project_id === projectId);
60608
- return json5({ templates, count: templates.length });
62397
+ return json6({ templates, count: templates.length });
60609
62398
  }
60610
62399
  if (!id && method === "POST") {
60611
62400
  const body2 = await readJson3(req);
@@ -60613,13 +62402,13 @@ async function handleV1Request(req, url, dependencies = {}) {
60613
62402
  if (!validated.ok)
60614
62403
  return error(400, validated.message);
60615
62404
  const template = await store.templates.create(validated.input, contextFromPrincipal(principal));
60616
- return json5({ template: await store.templates.getWithTasks(template.id) }, 201);
62405
+ return json6({ template: await store.templates.getWithTasks(template.id) }, 201);
60617
62406
  }
60618
62407
  if (!id)
60619
62408
  return error(405, `method ${method} not allowed on /v1/templates`);
60620
62409
  if (method === "GET") {
60621
62410
  const template = await store.templates.getWithTasks(id);
60622
- return template ? json5({ template }) : error(404, "template not found");
62411
+ return template ? json6({ template }) : error(404, "template not found");
60623
62412
  }
60624
62413
  if (method === "PATCH" || method === "PUT") {
60625
62414
  const body2 = await readJson3(req);
@@ -60627,18 +62416,18 @@ async function handleV1Request(req, url, dependencies = {}) {
60627
62416
  if (!validated.ok)
60628
62417
  return error(400, validated.message);
60629
62418
  const template = await store.templates.update(id, validated.patch, contextFromPrincipal(principal));
60630
- return template ? json5({ template: await store.templates.getWithTasks(id) }) : error(404, "template not found");
62419
+ return template ? json6({ template: await store.templates.getWithTasks(id) }) : error(404, "template not found");
60631
62420
  }
60632
62421
  if (method === "DELETE") {
60633
62422
  const deleted = await store.templates.delete(id, contextFromPrincipal(principal));
60634
- return deleted ? json5({ deleted: true, id }) : error(404, "template not found");
62423
+ return deleted ? json6({ deleted: true, id }) : error(404, "template not found");
60635
62424
  }
60636
62425
  return error(405, `method ${method} not allowed on /v1/templates/:id`);
60637
62426
  }
60638
62427
  if (resource === "agents") {
60639
62428
  if (!id && method === "GET") {
60640
62429
  const agents = await store.agents.list();
60641
- return json5({ agents, count: agents.length });
62430
+ return json6({ agents, count: agents.length });
60642
62431
  }
60643
62432
  if (!id && method === "POST") {
60644
62433
  const body2 = await readJson3(req);
@@ -60648,7 +62437,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60648
62437
  if (result && typeof result === "object" && "conflict" in result) {
60649
62438
  return error(409, result.message ?? "agent name conflict", { conflict: true });
60650
62439
  }
60651
- return json5({ agent: result }, 201);
62440
+ return json6({ agent: result }, 201);
60652
62441
  }
60653
62442
  if (id && action === "heartbeat") {
60654
62443
  if (method !== "POST")
@@ -60657,7 +62446,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60657
62446
  return error(501, "agent heartbeat is not supported by this storage backend");
60658
62447
  }
60659
62448
  const agent = await store.agents.heartbeat(id, contextFromPrincipal(principal));
60660
- return agent ? json5({ agent }) : error(404, "agent not found");
62449
+ return agent ? json6({ agent }) : error(404, "agent not found");
60661
62450
  }
60662
62451
  if (id && action === "release") {
60663
62452
  if (method !== "POST")
@@ -60672,11 +62461,11 @@ async function handleV1Request(req, url, dependencies = {}) {
60672
62461
  if (!result.released) {
60673
62462
  return error(409, "release denied: session_id does not match agent's current session", { released: false });
60674
62463
  }
60675
- return json5({ agent: result.agent, released: true });
62464
+ return json6({ agent: result.agent, released: true });
60676
62465
  }
60677
62466
  if (id && method === "GET") {
60678
62467
  const agent = await store.agents.get(id);
60679
- return agent ? json5({ agent }) : error(404, "agent not found");
62468
+ return agent ? json6({ agent }) : error(404, "agent not found");
60680
62469
  }
60681
62470
  }
60682
62471
  if (resource === "activity" && !id) {
@@ -60685,13 +62474,13 @@ async function handleV1Request(req, url, dependencies = {}) {
60685
62474
  const limitParam = url.searchParams.get("limit");
60686
62475
  const limit = limitParam ? Math.max(1, Math.min(1e4, Number(limitParam) || 50)) : 50;
60687
62476
  const activity = await store.audit.getRecentActivity(limit);
60688
- return json5({ activity, count: activity.length });
62477
+ return json6({ activity, count: activity.length });
60689
62478
  }
60690
62479
  if (resource === "task-lists") {
60691
62480
  if (!id && method === "GET") {
60692
62481
  const projectId = url.searchParams.get("project_id") ?? undefined;
60693
62482
  const taskLists = await store.taskLists.list(projectId);
60694
- return json5({ task_lists: taskLists, count: taskLists.length });
62483
+ return json6({ task_lists: taskLists, count: taskLists.length });
60695
62484
  }
60696
62485
  if (!id && method === "POST") {
60697
62486
  const body2 = await readJson3(req);
@@ -60713,17 +62502,17 @@ async function handleV1Request(req, url, dependencies = {}) {
60713
62502
  return error(400, "task-list slug must be non-empty kebab-case");
60714
62503
  }
60715
62504
  const taskList = await store.taskLists.create(body2, contextFromPrincipal(principal));
60716
- return json5({ task_list: taskList }, 201);
62505
+ return json6({ task_list: taskList }, 201);
60717
62506
  }
60718
62507
  if (id && method === "GET") {
60719
62508
  const taskList = await store.taskLists.get(id);
60720
- return taskList ? json5({ task_list: taskList }) : error(404, "task list not found");
62509
+ return taskList ? json6({ task_list: taskList }) : error(404, "task list not found");
60721
62510
  }
60722
62511
  if (id && (method === "PATCH" || method === "PUT")) {
60723
62512
  const body2 = await readJson3(req);
60724
62513
  if (!body2)
60725
62514
  return error(400, "invalid JSON body");
60726
- const unknownField = Object.keys(body2).find((key2) => !["slug", "name", "description", "metadata"].includes(key2));
62515
+ const unknownField = Object.keys(body2).find((key2) => !["slug", "name", "description", "metadata", "project_id"].includes(key2));
60727
62516
  if (unknownField)
60728
62517
  return error(400, `unsupported task-list update field: ${unknownField}`);
60729
62518
  if (Object.keys(body2).length === 0)
@@ -60737,14 +62526,26 @@ async function handleV1Request(req, url, dependencies = {}) {
60737
62526
  if (body2.metadata !== undefined && (!body2.metadata || typeof body2.metadata !== "object" || Array.isArray(body2.metadata))) {
60738
62527
  return error(400, "metadata must be an object");
60739
62528
  }
62529
+ if (body2.project_id !== undefined) {
62530
+ if (body2.project_id !== null && (typeof body2.project_id !== "string" || !body2.project_id.trim())) {
62531
+ return error(400, "project_id must be a non-empty string or null");
62532
+ }
62533
+ if (typeof body2.project_id === "string" && body2.project_id.trim() !== "") {
62534
+ const target = await store.projects.get(body2.project_id.trim());
62535
+ if (!target) {
62536
+ return error(400, `task-list project_id must reference a registered project: ${body2.project_id}`);
62537
+ }
62538
+ body2.project_id = target.id;
62539
+ }
62540
+ }
60740
62541
  if (!await store.taskLists.get(id))
60741
62542
  return error(404, "task list not found");
60742
62543
  const taskList = await store.taskLists.update(id, body2);
60743
- return json5({ task_list: taskList });
62544
+ return json6({ task_list: taskList });
60744
62545
  }
60745
62546
  if (id && method === "DELETE") {
60746
62547
  const deleted = await store.taskLists.delete(id, contextFromPrincipal(principal));
60747
- return deleted ? json5({ deleted: true, id }) : error(404, "task list not found");
62548
+ return deleted ? json6({ deleted: true, id }) : error(404, "task list not found");
60748
62549
  }
60749
62550
  return error(405, `method ${method} not allowed on /v1/task-lists${id ? "/:id" : ""}`);
60750
62551
  }
@@ -60755,7 +62556,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60755
62556
  return error(501, "dependency edge listing is not supported by this storage backend");
60756
62557
  }
60757
62558
  const dependencies2 = await store.dependencies.listAll();
60758
- return json5({ dependencies: dependencies2, count: dependencies2.length });
62559
+ return json6({ dependencies: dependencies2, count: dependencies2.length });
60759
62560
  }
60760
62561
  if (resource === "commits" && id) {
60761
62562
  if (method !== "GET")
@@ -60763,7 +62564,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60763
62564
  if (!store.commits)
60764
62565
  return error(501, "commit links are not supported by this storage backend");
60765
62566
  const commit = await store.commits.find(id);
60766
- return json5({ commit: commit ?? null });
62567
+ return json6({ commit: commit ?? null });
60767
62568
  }
60768
62569
  if (resource === "refs" && id) {
60769
62570
  if (method !== "GET")
@@ -60777,7 +62578,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60777
62578
  return error(400, "ref path segment has invalid percent encoding");
60778
62579
  }
60779
62580
  const refs = await store.gitRefs.find(decodedRef);
60780
- return json5({ refs, count: refs.length });
62581
+ return json6({ refs, count: refs.length });
60781
62582
  }
60782
62583
  if (resource === "next" && !id) {
60783
62584
  if (method !== "GET")
@@ -60789,7 +62590,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60789
62590
  ...url.searchParams.get("plan_id") ? { plan_id: url.searchParams.get("plan_id") } : {}
60790
62591
  };
60791
62592
  const task3 = await store.tasks.getNext(agent, filters);
60792
- return json5({ task: task3 ?? null });
62593
+ return json6({ task: task3 ?? null });
60793
62594
  }
60794
62595
  if (resource === "stats" && method === "GET") {
60795
62596
  const [tasks, tasksAll, projects] = await Promise.all([
@@ -60797,7 +62598,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60797
62598
  store.tasks.count({ include_subtasks: true }),
60798
62599
  store.projects.list()
60799
62600
  ]);
60800
- return json5({ tasks, tasks_all: tasksAll, subtasks: tasksAll - tasks, projects: projects.length });
62601
+ return json6({ tasks, tasks_all: tasksAll, subtasks: tasksAll - tasks, projects: projects.length });
60801
62602
  }
60802
62603
  if (resource === "integrity" && !id) {
60803
62604
  if (method !== "GET")
@@ -60806,7 +62607,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60806
62607
  return error(501, "referential-integrity reporting is not supported by this storage backend");
60807
62608
  }
60808
62609
  const integrity = await store.integrity.report();
60809
- return json5({ integrity });
62610
+ return json6({ integrity });
60810
62611
  }
60811
62612
  if (resource === "import") {
60812
62613
  if (method !== "POST")
@@ -60831,7 +62632,7 @@ async function handleV1Request(req, url, dependencies = {}) {
60831
62632
  }
60832
62633
  const operation = completionImports.operations[0];
60833
62634
  const completed = await store.plans.completeAtRevision(operation.id, operation.expected_updated_at, contextFromPrincipal(principal));
60834
- return json5({
62635
+ return json6({
60835
62636
  result: {
60836
62637
  inserted: 0,
60837
62638
  updated: completed.applied ? 1 : 0,
@@ -60870,17 +62671,17 @@ async function handleV1Request(req, url, dependencies = {}) {
60870
62671
  audit_history_id: failure.auditHistoryId
60871
62672
  });
60872
62673
  }
60873
- return json5({ result, received });
62674
+ return json6({ result, received });
60874
62675
  }
60875
62676
  return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
60876
62677
  } catch (e) {
60877
62678
  if (e instanceof PlanProjectLinkError) {
60878
- const status2 = e.code === "PLAN_PROJECT_LINK_PLAN_NOT_FOUND" || e.code === "PLAN_PROJECT_LINK_PROJECT_NOT_FOUND" || e.code === "PLAN_PROJECT_LINK_RECEIPT_NOT_FOUND" ? 404 : e.code === "PLAN_PROJECT_LINK_IDEMPOTENCY_KEY_INVALID" ? 400 : e.code === "PLAN_PROJECT_LINK_UNSUPPORTED" ? 501 : 409;
60879
- return error(status2, e.message, { code: e.code, conflict: status2 === 409, ...e.details });
62679
+ const status3 = e.code === "PLAN_PROJECT_LINK_PLAN_NOT_FOUND" || e.code === "PLAN_PROJECT_LINK_PROJECT_NOT_FOUND" || e.code === "PLAN_PROJECT_LINK_RECEIPT_NOT_FOUND" ? 404 : e.code === "PLAN_PROJECT_LINK_IDEMPOTENCY_KEY_INVALID" ? 400 : e.code === "PLAN_PROJECT_LINK_UNSUPPORTED" ? 501 : 409;
62680
+ return error(status3, e.message, { code: e.code, conflict: status3 === 409, ...e.details });
60880
62681
  }
60881
62682
  if (e instanceof ProjectTaskListEnsureError) {
60882
- const status2 = e.code === "PROJECT_NOT_FOUND" || e.code === "PROJECT_TASK_LIST_RECEIPT_NOT_FOUND" ? 404 : e.code === "PROJECT_TASK_LIST_IDEMPOTENCY_KEY_INVALID" ? 400 : 409;
60883
- return error(status2, e.message, { code: e.code, conflict: status2 === 409, ...e.details });
62683
+ const status3 = e.code === "PROJECT_NOT_FOUND" || e.code === "PROJECT_TASK_LIST_RECEIPT_NOT_FOUND" ? 404 : e.code === "PROJECT_TASK_LIST_IDEMPOTENCY_KEY_INVALID" ? 400 : 409;
62684
+ return error(status3, e.message, { code: e.code, conflict: status3 === 409, ...e.details });
60884
62685
  }
60885
62686
  if (e instanceof TaskReferenceAmbiguousError) {
60886
62687
  return error(409, e.message, {
@@ -60902,10 +62703,10 @@ async function handleV1Request(req, url, dependencies = {}) {
60902
62703
  });
60903
62704
  }
60904
62705
  if (e instanceof StaleLockHandoffError) {
60905
- const status2 = e.code === "STALE_LOCK_HANDOFF_INVALID_TASK_ID" || e.code === "STALE_LOCK_HANDOFF_INVALID_INPUT" ? 400 : e.code === "STALE_LOCK_HANDOFF_ACTOR_MISMATCH" ? 403 : 409;
60906
- return error(status2, e.message, {
62706
+ const status3 = e.code === "STALE_LOCK_HANDOFF_INVALID_TASK_ID" || e.code === "STALE_LOCK_HANDOFF_INVALID_INPUT" ? 400 : e.code === "STALE_LOCK_HANDOFF_ACTOR_MISMATCH" ? 403 : 409;
62707
+ return error(status3, e.message, {
60907
62708
  code: e.code,
60908
- conflict: status2 === 409,
62709
+ conflict: status3 === 409,
60909
62710
  ...e.details
60910
62711
  });
60911
62712
  }
@@ -60933,19 +62734,20 @@ async function handleV1Request(req, url, dependencies = {}) {
60933
62734
  return error(500, e.message || "internal error");
60934
62735
  }
60935
62736
  }
60936
- var JSON_HEADERS4, DEFAULT_COMMENT_PAGE_SIZE = 100, MAX_COMMENT_PAGE_SIZE = 500, LEGACY_COMMENT_RESPONSE_LIMIT = 500, RFC3339_DATE_TIME;
62737
+ var JSON_HEADERS5, DEFAULT_COMMENT_PAGE_SIZE = 100, MAX_COMMENT_PAGE_SIZE = 500, LEGACY_COMMENT_RESPONSE_LIMIT = 500, RFC3339_DATE_TIME;
60937
62738
  var init_v1 = __esm(() => {
60938
62739
  init_types();
60939
62740
  init_cloud();
60940
62741
  init_pr_groups();
60941
62742
  init_project_registration();
60942
62743
  init_task_manifest();
62744
+ init_task_subtree_transfer();
60943
62745
  init_redaction();
60944
62746
  init_project_task_list_ensure();
60945
62747
  init_plan_project_link();
60946
62748
  init_stale_lock_handoff();
60947
62749
  init_audit_history_import();
60948
- JSON_HEADERS4 = { "Content-Type": "application/json" };
62750
+ JSON_HEADERS5 = { "Content-Type": "application/json" };
60949
62751
  RFC3339_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
60950
62752
  });
60951
62753
 
@@ -61156,7 +62958,7 @@ class SqlitePrGroupLedgerPersistence {
61156
62958
  }
61157
62959
  }
61158
62960
  var sqliteTransactionTails2;
61159
- var init_sqlite3 = __esm(() => {
62961
+ var init_sqlite4 = __esm(() => {
61160
62962
  sqliteTransactionTails2 = new WeakMap;
61161
62963
  });
61162
62964
 
@@ -61187,10 +62989,10 @@ function createLocalPrGroupLedger(db = getDatabase()) {
61187
62989
  var init_pr_groups2 = __esm(() => {
61188
62990
  init_database();
61189
62991
  init_ledger();
61190
- init_sqlite3();
62992
+ init_sqlite4();
61191
62993
  init_types3();
61192
62994
  init_ledger();
61193
- init_sqlite3();
62995
+ init_sqlite4();
61194
62996
  init_http_client();
61195
62997
  init_postgres();
61196
62998
  });
@@ -61201,7 +63003,7 @@ __export(exports_serve, {
61201
63003
  taskToSummary: () => taskToSummary,
61202
63004
  startServer: () => startServer,
61203
63005
  serveStaticFile: () => serveStaticFile,
61204
- json: () => json3,
63006
+ json: () => json4,
61205
63007
  checkAuth: () => checkAuth,
61206
63008
  SECURITY_HEADERS: () => SECURITY_HEADERS,
61207
63009
  MIME_TYPES: () => MIME_TYPES
@@ -61287,9 +63089,9 @@ function checkRateLimit(ip) {
61287
63089
  }
61288
63090
  return { allowed: true };
61289
63091
  }
61290
- function json3(data, status2 = 200, headers) {
63092
+ function json4(data, status3 = 200, headers) {
61291
63093
  return new Response(JSON.stringify(data), {
61292
- status: status2,
63094
+ status: status3,
61293
63095
  headers: {
61294
63096
  "Content-Type": "application/json",
61295
63097
  ...SECURITY_HEADERS,
@@ -61432,7 +63234,7 @@ Dashboard not found at: ${dashboardDir}`);
61432
63234
  "Access-Control-Allow-Headers": "Content-Type, X-API-Key, Authorization",
61433
63235
  Vary: "Origin"
61434
63236
  } : undefined;
61435
- const jsonWithCors = (data, status2 = 200) => json3(data, status2, corsHeaders);
63237
+ const jsonWithCors = (data, status3 = 200) => json4(data, status3, corsHeaders);
61436
63238
  if (method === "OPTIONS") {
61437
63239
  return new Response(null, {
61438
63240
  headers: corsHeaders || {
@@ -61513,13 +63315,13 @@ Dashboard not found at: ${dashboardDir}`);
61513
63315
  return res;
61514
63316
  }
61515
63317
  if (path === "/api/health" && method === "GET") {
61516
- return handleHealth(ctx, json3);
63318
+ return handleHealth(ctx, json4);
61517
63319
  }
61518
63320
  if (path === "/api/headless" && method === "GET") {
61519
- return handleHeadlessBoundary(ctx, json3);
63321
+ return handleHeadlessBoundary(ctx, json4);
61520
63322
  }
61521
63323
  if (path === "/api/stats" && method === "GET") {
61522
- return handleStats(ctx, json3);
63324
+ return handleStats(ctx, json4);
61523
63325
  }
61524
63326
  if (path === "/api/tasks" && method === "GET") {
61525
63327
  return handleListTasks(req, url, ctx, jsonWithCors, taskToSummary);
@@ -61534,16 +63336,16 @@ Dashboard not found at: ${dashboardDir}`);
61534
63336
  return handleTasksExport(req, url, ctx, jsonWithCors, taskToSummary);
61535
63337
  }
61536
63338
  if (path === "/api/tasks/bulk" && method === "POST") {
61537
- return handleTasksBulk(req, ctx, json3);
63339
+ return handleTasksBulk(req, ctx, json4);
61538
63340
  }
61539
63341
  if (path === "/api/tasks/status" && method === "GET") {
61540
- return handleTasksStatus(req, url, ctx, json3);
63342
+ return handleTasksStatus(req, url, ctx, json4);
61541
63343
  }
61542
63344
  if (path === "/api/tasks/next" && method === "GET") {
61543
63345
  return handleTasksNext(req, url, ctx, jsonWithCors, taskToSummary);
61544
63346
  }
61545
63347
  if (path === "/api/tasks/active" && method === "GET") {
61546
- return handleTasksActive(req, url, ctx, json3);
63348
+ return handleTasksActive(req, url, ctx, json4);
61547
63349
  }
61548
63350
  if (path === "/api/tasks/stale" && method === "GET") {
61549
63351
  return handleTasksStale(req, url, ctx, jsonWithCors, taskToSummary);
@@ -61556,11 +63358,11 @@ Dashboard not found at: ${dashboardDir}`);
61556
63358
  }
61557
63359
  const attachmentsMatch = path.match(/^\/api\/tasks\/([^/]+)\/attachments$/);
61558
63360
  if (attachmentsMatch && method === "GET") {
61559
- return handleTaskAttachments(attachmentsMatch[1], ctx, json3);
63361
+ return handleTaskAttachments(attachmentsMatch[1], ctx, json4);
61560
63362
  }
61561
63363
  const progressMatch = path.match(/^\/api\/tasks\/([^/]+)\/progress$/);
61562
63364
  if (progressMatch) {
61563
- const res = await handleTaskProgress(progressMatch[1], req, method, ctx, json3, url);
63365
+ const res = await handleTaskProgress(progressMatch[1], req, method, ctx, json4, url);
61564
63366
  if (res !== null)
61565
63367
  return res;
61566
63368
  }
@@ -61574,7 +63376,7 @@ Dashboard not found at: ${dashboardDir}`);
61574
63376
  return handlePatchTask(id, req, ctx, jsonWithCors, taskToSummary);
61575
63377
  }
61576
63378
  if (method === "DELETE") {
61577
- return handleDeleteTask(id, ctx, json3);
63379
+ return handleDeleteTask(id, ctx, json4);
61578
63380
  }
61579
63381
  }
61580
63382
  const startMatch = path.match(/^\/api\/tasks\/([^/]+)\/start$/);
@@ -61590,7 +63392,7 @@ Dashboard not found at: ${dashboardDir}`);
61590
63392
  return handleCompleteTask(completeMatch[1], ctx, jsonWithCors, taskToSummary);
61591
63393
  }
61592
63394
  if (path === "/api/projects" && method === "GET") {
61593
- return handleListProjects(url, ctx, json3);
63395
+ return handleListProjects(url, ctx, json4);
61594
63396
  }
61595
63397
  if (path === "/api/agents/me" && method === "GET") {
61596
63398
  return handleAgentMe(req, url, ctx, jsonWithCors, taskToSummary);
@@ -61603,92 +63405,92 @@ Dashboard not found at: ${dashboardDir}`);
61603
63405
  return handleClaimTask(req, ctx, jsonWithCors, taskToSummary);
61604
63406
  }
61605
63407
  if (path === "/api/orgs" && method === "GET") {
61606
- return handleListOrgs(ctx, json3);
63408
+ return handleListOrgs(ctx, json4);
61607
63409
  }
61608
63410
  if (path === "/api/orgs" && method === "POST") {
61609
- return handleCreateOrg(req, ctx, json3);
63411
+ return handleCreateOrg(req, ctx, json4);
61610
63412
  }
61611
63413
  const orgMatch = path.match(/^\/api\/orgs\/([^/]+)$/);
61612
63414
  if (orgMatch && method === "PATCH") {
61613
- return handleUpdateOrg(orgMatch[1], req, ctx, json3);
63415
+ return handleUpdateOrg(orgMatch[1], req, ctx, json4);
61614
63416
  }
61615
63417
  if (orgMatch && method === "DELETE") {
61616
- return handleDeleteOrg(orgMatch[1], ctx, json3);
63418
+ return handleDeleteOrg(orgMatch[1], ctx, json4);
61617
63419
  }
61618
63420
  if (path === "/api/org" && method === "GET") {
61619
- return handleOrgChart(ctx, json3);
63421
+ return handleOrgChart(ctx, json4);
61620
63422
  }
61621
63423
  const teamMatch = path.match(/^\/api\/agents\/([^/]+)\/team$/);
61622
63424
  if (teamMatch && method === "GET") {
61623
- return handleAgentTeam(teamMatch[1], ctx, json3);
63425
+ return handleAgentTeam(teamMatch[1], ctx, json4);
61624
63426
  }
61625
63427
  if (path === "/api/agents" && method === "GET") {
61626
- return handleListAgents(url, ctx, json3);
63428
+ return handleListAgents(url, ctx, json4);
61627
63429
  }
61628
63430
  if (path === "/api/projects" && method === "POST") {
61629
- return handleCreateProject(req, ctx, json3);
63431
+ return handleCreateProject(req, ctx, json4);
61630
63432
  }
61631
63433
  const projectDeleteMatch = path.match(/^\/api\/projects\/([^/]+)$/);
61632
63434
  if (projectDeleteMatch && method === "DELETE") {
61633
- return handleDeleteProject(projectDeleteMatch[1], ctx, json3);
63435
+ return handleDeleteProject(projectDeleteMatch[1], ctx, json4);
61634
63436
  }
61635
63437
  if (path === "/api/agents" && method === "POST") {
61636
- return handleRegisterAgent(req, ctx, json3);
63438
+ return handleRegisterAgent(req, ctx, json4);
61637
63439
  }
61638
63440
  const agentMatch = path.match(/^\/api\/agents\/([^/]+)$/);
61639
63441
  if (agentMatch && method === "PATCH") {
61640
- return handleUpdateAgent(agentMatch[1], req, ctx, json3);
63442
+ return handleUpdateAgent(agentMatch[1], req, ctx, json4);
61641
63443
  }
61642
63444
  if (agentMatch && method === "DELETE") {
61643
- return handleDeleteAgent(agentMatch[1], ctx, json3);
63445
+ return handleDeleteAgent(agentMatch[1], ctx, json4);
61644
63446
  }
61645
63447
  if (path === "/api/agents/bulk" && method === "POST") {
61646
- return handleBulkDeleteAgents(req, ctx, json3);
63448
+ return handleBulkDeleteAgents(req, ctx, json4);
61647
63449
  }
61648
63450
  if (path === "/api/projects/bulk" && method === "POST") {
61649
- return handleBulkDeleteProjects(req, ctx, json3);
63451
+ return handleBulkDeleteProjects(req, ctx, json4);
61650
63452
  }
61651
63453
  if (path === "/api/doctor" && method === "GET") {
61652
- return handleDoctor(ctx, json3);
63454
+ return handleDoctor(ctx, json4);
61653
63455
  }
61654
63456
  if (path === "/api/report" && method === "GET") {
61655
- return handleReport(req, url, ctx, json3);
63457
+ return handleReport(req, url, ctx, json4);
61656
63458
  }
61657
63459
  if (path === "/api/activity" && method === "GET") {
61658
- return handleActivity(req, url, ctx, json3);
63460
+ return handleActivity(req, url, ctx, json4);
61659
63461
  }
61660
63462
  const historyMatch = path.match(/^\/api\/tasks\/([^/]+)\/history$/);
61661
63463
  if (historyMatch && method === "GET") {
61662
- return handleTaskHistory(historyMatch[1], ctx, json3, url);
63464
+ return handleTaskHistory(historyMatch[1], ctx, json4, url);
61663
63465
  }
61664
63466
  if (path === "/api/webhooks" && method === "GET") {
61665
- return handleListWebhooks(ctx, json3);
63467
+ return handleListWebhooks(ctx, json4);
61666
63468
  }
61667
63469
  if (path === "/api/webhooks" && method === "POST") {
61668
- return handleCreateWebhook(req, ctx, json3);
63470
+ return handleCreateWebhook(req, ctx, json4);
61669
63471
  }
61670
63472
  const webhookMatch = path.match(/^\/api\/webhooks\/([^/]+)$/);
61671
63473
  if (webhookMatch && method === "DELETE") {
61672
- return handleDeleteWebhook(webhookMatch[1], ctx, json3);
63474
+ return handleDeleteWebhook(webhookMatch[1], ctx, json4);
61673
63475
  }
61674
63476
  if (path === "/api/templates" && method === "GET") {
61675
- return handleListTemplates(ctx, json3);
63477
+ return handleListTemplates(ctx, json4);
61676
63478
  }
61677
63479
  if (path === "/api/templates" && method === "POST") {
61678
- return handleCreateTemplate(req, ctx, json3);
63480
+ return handleCreateTemplate(req, ctx, json4);
61679
63481
  }
61680
63482
  const templateMatch = path.match(/^\/api\/templates\/([^/]+)$/);
61681
63483
  if (templateMatch && method === "DELETE") {
61682
- return handleDeleteTemplate(templateMatch[1], ctx, json3);
63484
+ return handleDeleteTemplate(templateMatch[1], ctx, json4);
61683
63485
  }
61684
63486
  if (path === "/api/plans" && method === "GET") {
61685
- return handleListPlans(url, ctx, json3);
63487
+ return handleListPlans(url, ctx, json4);
61686
63488
  }
61687
63489
  if (path === "/api/plans" && method === "POST") {
61688
- return handleCreatePlan(req, ctx, json3);
63490
+ return handleCreatePlan(req, ctx, json4);
61689
63491
  }
61690
63492
  if (path === "/api/plans/bulk" && method === "POST") {
61691
- return handleBulkDeletePlans(req, ctx, json3);
63493
+ return handleBulkDeletePlans(req, ctx, json4);
61692
63494
  }
61693
63495
  const planMatch = path.match(/^\/api\/plans\/([^/]+)$/);
61694
63496
  if (planMatch) {
@@ -61697,16 +63499,16 @@ Dashboard not found at: ${dashboardDir}`);
61697
63499
  return handleGetPlan(id, ctx, jsonWithCors, taskToSummary);
61698
63500
  }
61699
63501
  if (method === "PATCH") {
61700
- return handleUpdatePlan(id, req, ctx, json3);
63502
+ return handleUpdatePlan(id, req, ctx, json4);
61701
63503
  }
61702
63504
  if (method === "DELETE") {
61703
- return handleDeletePlan(id, ctx, json3);
63505
+ return handleDeletePlan(id, ctx, json4);
61704
63506
  }
61705
63507
  }
61706
63508
  const staticRes = handleStaticFiles(path, method, ctx, jsonWithCors, serveStaticFile);
61707
63509
  if (staticRes)
61708
63510
  return staticRes;
61709
- return json3({ error: "Not found" }, 404);
63511
+ return json4({ error: "Not found" }, 404);
61710
63512
  }
61711
63513
  });
61712
63514
  const shutdown = () => {