@hasna/todos 0.16.0 → 0.17.0

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 (70) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +59 -22
  3. package/dist/cli/cloud-router.d.ts +63 -21
  4. package/dist/cli/cloud-router.d.ts.map +1 -1
  5. package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/template-commands.d.ts.map +1 -1
  7. package/dist/cli/index.js +2591 -503
  8. package/dist/cli/stage-a.d.ts +2 -12
  9. package/dist/cli/stage-a.d.ts.map +1 -1
  10. package/dist/cli/template-remote.d.ts +90 -0
  11. package/dist/cli/template-remote.d.ts.map +1 -0
  12. package/dist/cli-mcp-parity.d.ts.map +1 -1
  13. package/dist/contracts.js +77 -30
  14. package/dist/db/agents.d.ts +8 -0
  15. package/dist/db/agents.d.ts.map +1 -1
  16. package/dist/db/database.d.ts +6 -0
  17. package/dist/db/database.d.ts.map +1 -1
  18. package/dist/db/identity-mapping.d.ts +25 -0
  19. package/dist/db/identity-mapping.d.ts.map +1 -1
  20. package/dist/db/task-crud.d.ts.map +1 -1
  21. package/dist/index.js +496 -58
  22. package/dist/lib/feature-manifest.d.ts.map +1 -1
  23. package/dist/lib/local-opt-in.d.ts +14 -0
  24. package/dist/lib/local-opt-in.d.ts.map +1 -1
  25. package/dist/lib/paths.d.ts +11 -1
  26. package/dist/lib/paths.d.ts.map +1 -1
  27. package/dist/mcp/index.d.ts +52 -0
  28. package/dist/mcp/index.d.ts.map +1 -1
  29. package/dist/mcp/index.js +15162 -30695
  30. package/dist/mcp/tools/agents.d.ts.map +1 -1
  31. package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -1
  32. package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
  33. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  34. package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
  35. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
  36. package/dist/mcp/tools/task-resources.d.ts.map +1 -1
  37. package/dist/mcp/tools/templates.d.ts.map +1 -1
  38. package/dist/mcp.js +3 -3
  39. package/dist/project-registration.js +466 -35
  40. package/dist/registry.js +77 -30
  41. package/dist/release-provenance.json +5 -5
  42. package/dist/sdk/client.d.ts +3 -1
  43. package/dist/sdk/client.d.ts.map +1 -1
  44. package/dist/sdk/index.js +48 -12
  45. package/dist/sdk/resolve.d.ts +23 -13
  46. package/dist/sdk/resolve.d.ts.map +1 -1
  47. package/dist/sdk/types.d.ts +17 -2
  48. package/dist/sdk/types.d.ts.map +1 -1
  49. package/dist/sdk/v1.generated.d.ts +115 -0
  50. package/dist/sdk/v1.generated.d.ts.map +1 -1
  51. package/dist/server/index.js +3972 -916
  52. package/dist/server/openapi.d.ts +837 -50
  53. package/dist/server/openapi.d.ts.map +1 -1
  54. package/dist/server/rate-limit-config.d.ts +6 -0
  55. package/dist/server/rate-limit-config.d.ts.map +1 -0
  56. package/dist/server/serve.d.ts.map +1 -1
  57. package/dist/server/v1.d.ts.map +1 -1
  58. package/dist/storage/index.d.ts +1 -1
  59. package/dist/storage/index.d.ts.map +1 -1
  60. package/dist/storage/interfaces.d.ts +74 -3
  61. package/dist/storage/interfaces.d.ts.map +1 -1
  62. package/dist/storage/local-sqlite.d.ts.map +1 -1
  63. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  64. package/dist/storage.js +464 -33
  65. package/dist/task-manifest.js +8 -2
  66. package/dist/types/index.d.ts +5 -2
  67. package/dist/types/index.d.ts.map +1 -1
  68. package/package.json +2 -2
  69. package/dist/mcp/tools/code-tools.d.ts +0 -15
  70. package/dist/mcp/tools/code-tools.d.ts.map +0 -1
package/dist/cli/index.js CHANGED
@@ -2123,7 +2123,7 @@ var package_default;
2123
2123
  var init_package = __esm(() => {
2124
2124
  package_default = {
2125
2125
  name: "@hasna/todos",
2126
- version: "0.16.0",
2126
+ version: "0.17.0",
2127
2127
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
2128
2128
  type: "module",
2129
2129
  main: "dist/index.js",
@@ -2236,7 +2236,7 @@ var init_package = __esm(() => {
2236
2236
  url: "https://github.com/hasna/apps.git",
2237
2237
  directory: "apps/todos"
2238
2238
  },
2239
- homepage: "https://github.com/hasna/apps",
2239
+ homepage: "https://github.com/hasna/apps/tree/main/apps/todos#readme",
2240
2240
  bugs: {
2241
2241
  url: "https://github.com/hasna/apps/issues"
2242
2242
  },
@@ -2566,7 +2566,7 @@ var init_types = __esm(() => {
2566
2566
  // src/lib/paths.ts
2567
2567
  import { existsSync } from "fs";
2568
2568
  import { homedir } from "os";
2569
- import { join, resolve } from "path";
2569
+ import { isAbsolute, join, resolve } from "path";
2570
2570
  import { homedir as pathsResolverHomedir } from "os";
2571
2571
  import { join as pathsResolverJoin } from "path";
2572
2572
  function pathsResolverAssertApp(app) {
@@ -2623,8 +2623,14 @@ function dataDir(options) {
2623
2623
  function effectiveHome(env = process.env) {
2624
2624
  return env.HOME || env.USERPROFILE || homedir();
2625
2625
  }
2626
+ function hasnaHomeRoot(env = process.env) {
2627
+ const override = env["HASNA_HOME"]?.trim();
2628
+ if (override && isAbsolute(override))
2629
+ return override;
2630
+ return join(effectiveHome(env), ".hasna");
2631
+ }
2626
2632
  function legacyHomeDir(env = process.env) {
2627
- return join(effectiveHome(env), ".hasna", "todos");
2633
+ return join(hasnaHomeRoot(env), "todos");
2628
2634
  }
2629
2635
  function resolverHome(env = process.env) {
2630
2636
  return dataDir({ app: "todos", home: effectiveHome(env), env });
@@ -5881,6 +5887,12 @@ import {
5881
5887
  function isTodosLocalOptIn(env = process.env) {
5882
5888
  return TODOS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
5883
5889
  }
5890
+ function todosLocalModeNotice(reason = "local-opt-in") {
5891
+ if (reason === "local-only-command") {
5892
+ return "todos: LOCAL mode \u2014 this command only ever runs against the on-box SQLite store, so this run " + "does not reach the hosted fleet even though a Todos authority is configured.";
5893
+ }
5894
+ return "todos: LOCAL mode \u2014 using the on-box SQLite store, not the hosted fleet " + "(HASNA_TODOS_LOCAL is set). Unset it, and provide a credential via the Keychain item " + "hasna.credentials.todos.api-key, ~/.hasna/todos/config/credentials, or HASNA_TODOS_API_KEY, " + "to work against https://api.hasna.com/todos.";
5895
+ }
5884
5896
  function todosAuthorityEnvKeys() {
5885
5897
  const keys = clientTransportEnvKeys("todos");
5886
5898
  return [
@@ -6036,6 +6048,10 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env, options = {}) {
6036
6048
  };
6037
6049
  }
6038
6050
  function classifyRemoteRequestError(baseUrl, route, error) {
6051
+ const errorName = error instanceof Error ? error.name : "";
6052
+ if (errorName === "CredentialResolutionError" || errorName === "CredentialFileUnsafeError") {
6053
+ rethrowAuthorityFailure(error);
6054
+ }
6039
6055
  const status = error && typeof error === "object" ? error.status : undefined;
6040
6056
  if (status === 401) {
6041
6057
  throw new Error(`REMOTE_API_UNAUTHORIZED: configured Todos authority ${baseUrl} rejected HASNA_TODOS_API_KEY for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
@@ -6359,6 +6375,10 @@ function toListQuery(filter = {}) {
6359
6375
  query["parent_id"] = filter.parent_id ?? "";
6360
6376
  if (filter.include_subtasks !== undefined)
6361
6377
  query["include_subtasks"] = filter.include_subtasks ? "true" : "false";
6378
+ if (filter.include_archived !== undefined)
6379
+ query["include_archived"] = filter.include_archived ? "true" : "false";
6380
+ if (filter.archived_only !== undefined)
6381
+ query["archived_only"] = filter.archived_only ? "true" : "false";
6362
6382
  if (filter.plan_id)
6363
6383
  query["plan_id"] = filter.plan_id;
6364
6384
  if (filter.task_list_id)
@@ -6559,6 +6579,143 @@ async function cloudListTasks(client, filter = {}) {
6559
6579
  union.sort(compareCloudTaskOrder);
6560
6580
  return union.slice(start, windowEnd);
6561
6581
  }
6582
+ async function cloudListArchivedTasksPage(client, options = {}) {
6583
+ const limit = options.limit ?? 50;
6584
+ const offset = options.offset ?? 0;
6585
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) {
6586
+ throw new Error("archived task limit must be an integer from 1 to 200");
6587
+ }
6588
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > 1e4) {
6589
+ throw new Error("archived task offset must be an integer from 0 to 10000");
6590
+ }
6591
+ const page = await requestRawCloudTaskPage(client, {
6592
+ include_archived: true,
6593
+ archived_only: true,
6594
+ include_subtasks: true,
6595
+ ...options.project_id ? { project_id: options.project_id } : {},
6596
+ limit,
6597
+ offset
6598
+ });
6599
+ if (page.total === undefined || page.tasks.length > limit || offset + page.tasks.length > page.total) {
6600
+ throw new Error("REMOTE_API_INCOMPATIBLE: archived-only task page lacks a consistent authoritative total");
6601
+ }
6602
+ const seen = new Set;
6603
+ for (const task of page.tasks) {
6604
+ if (!task || typeof task.id !== "string" || !task.id || !task.archived_at || seen.has(task.id) || options.project_id !== undefined && task.project_id !== options.project_id) {
6605
+ throw new Error("REMOTE_API_INCOMPATIBLE: archived-only task page returned a malformed, repeated, or out-of-scope row");
6606
+ }
6607
+ seen.add(task.id);
6608
+ }
6609
+ const hasMore = offset + page.tasks.length < page.total;
6610
+ if (hasMore && page.tasks.length === 0) {
6611
+ throw new Error("REMOTE_API_INCOMPATIBLE: archived-only task pagination stalled before its authoritative total");
6612
+ }
6613
+ return {
6614
+ tasks: page.tasks,
6615
+ total: page.total,
6616
+ limit,
6617
+ offset,
6618
+ has_more: hasMore,
6619
+ next_offset: hasMore ? offset + page.tasks.length : null
6620
+ };
6621
+ }
6622
+ async function cloudListTasksCompleteBounded(client, filter, maxTasks = 1e4) {
6623
+ if (!Number.isSafeInteger(maxTasks) || maxTasks < 1 || maxTasks > 1e4) {
6624
+ throw new Error("maxTasks must be an integer from 1 to 10000");
6625
+ }
6626
+ if ((filter.offset ?? 0) !== 0)
6627
+ throw new Error("complete bounded task reads require offset zero");
6628
+ if (Array.isArray(filter.status)) {
6629
+ if (filter.status.length === 0) {
6630
+ const { status: _status2, ...unfiltered } = filter;
6631
+ return cloudListTasksCompleteBounded(client, unfiltered, maxTasks);
6632
+ }
6633
+ const combined = [];
6634
+ const ids2 = new Set;
6635
+ const { status: _status, ...baseFilter } = filter;
6636
+ for (const status of filter.status) {
6637
+ const remaining = maxTasks - combined.length;
6638
+ if (remaining < 1)
6639
+ throw new Error(`REMOTE_RESULT_TOO_LARGE: /v1/tasks exceeds the bounded limit ${maxTasks}`);
6640
+ const rows = await cloudListTasksCompleteBounded(client, { ...baseFilter, status }, remaining);
6641
+ for (const task of rows) {
6642
+ if (ids2.has(task.id)) {
6643
+ throw new Error("REMOTE_API_INCOMPATIBLE: scalar status pages repeated a task across disjoint statuses");
6644
+ }
6645
+ ids2.add(task.id);
6646
+ combined.push(task);
6647
+ }
6648
+ }
6649
+ combined.sort(compareCloudTaskOrder);
6650
+ return combined;
6651
+ }
6652
+ const pageSize = Math.min(500, maxTasks);
6653
+ const base = { ...filter, offset: undefined, limit: undefined };
6654
+ const first = await requestRawCloudTaskPage(client, { ...base, limit: pageSize, offset: 0 });
6655
+ if (first.total === undefined) {
6656
+ throw new Error("REMOTE_API_INCOMPATIBLE: complete bounded task reads require an authoritative total");
6657
+ }
6658
+ const total = first.total;
6659
+ if (!Number.isSafeInteger(total) || total < 0 || total > maxTasks) {
6660
+ throw new Error(`REMOTE_RESULT_TOO_LARGE: /v1/tasks reports ${String(total)} rows above the bounded limit ${maxTasks}`);
6661
+ }
6662
+ const tasks = [];
6663
+ const ids = new Set;
6664
+ const statuses = filter.status === undefined ? null : new Set(Array.isArray(filter.status) ? filter.status : [filter.status]);
6665
+ const priorities = filter.priority === undefined ? null : new Set(Array.isArray(filter.priority) ? filter.priority : [filter.priority]);
6666
+ const matchesSelection = (task) => {
6667
+ if (statuses && !statuses.has(task.status))
6668
+ return false;
6669
+ if (priorities && !priorities.has(task.priority))
6670
+ return false;
6671
+ if (filter.project_id !== undefined && task.project_id !== filter.project_id)
6672
+ return false;
6673
+ if (filter.parent_id !== undefined && (task.parent_id ?? null) !== filter.parent_id)
6674
+ return false;
6675
+ if (filter.plan_id !== undefined && task.plan_id !== filter.plan_id)
6676
+ return false;
6677
+ if (filter.task_list_id !== undefined && task.task_list_id !== filter.task_list_id)
6678
+ return false;
6679
+ if (filter.assigned_to !== undefined && task.assigned_to !== filter.assigned_to)
6680
+ return false;
6681
+ if (filter.agent_id !== undefined && task.agent_id !== filter.agent_id)
6682
+ return false;
6683
+ if (filter.archived_only === true && !task.archived_at)
6684
+ return false;
6685
+ if (filter.include_archived === false && task.archived_at)
6686
+ return false;
6687
+ if (filter.include_subtasks !== true && filter.parent_id === undefined && task.parent_id)
6688
+ return false;
6689
+ return true;
6690
+ };
6691
+ const append = (page) => {
6692
+ for (const task of page) {
6693
+ if (!task || typeof task.id !== "string" || !task.id || ids.has(task.id)) {
6694
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks returned a malformed or repeated task page");
6695
+ }
6696
+ if (!matchesSelection(task)) {
6697
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks returned a row outside the requested bounded selection");
6698
+ }
6699
+ ids.add(task.id);
6700
+ tasks.push(task);
6701
+ }
6702
+ };
6703
+ append(first.tasks);
6704
+ if (tasks.length > total)
6705
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks returned more rows than its total");
6706
+ while (tasks.length < total) {
6707
+ const page = await requestRawCloudTaskPage(client, {
6708
+ ...base,
6709
+ limit: Math.min(pageSize, total - tasks.length),
6710
+ offset: tasks.length
6711
+ });
6712
+ if (page.total !== total || page.tasks.length === 0 || tasks.length + page.tasks.length > total) {
6713
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks pagination changed or stopped before the authoritative total");
6714
+ }
6715
+ append(page.tasks);
6716
+ }
6717
+ return tasks;
6718
+ }
6562
6719
  async function cloudResolveTaskRef(client, ref) {
6563
6720
  const input = ref.trim().toLowerCase();
6564
6721
  if (!input)
@@ -6592,6 +6749,10 @@ async function cloudGetTask(client, id) {
6592
6749
  async function cloudCreateTask(client, input, verification = {}) {
6593
6750
  const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
6594
6751
  const expectedPlanId = typeof input["plan_id"] === "string" ? input["plan_id"] : null;
6752
+ const expectedTaskListId = typeof input["task_list_id"] === "string" ? input["task_list_id"] : null;
6753
+ const expectedAssignedTo = typeof input["assigned_to"] === "string" ? input["assigned_to"] : null;
6754
+ const verifyTaskListId = expectedTaskListId !== null;
6755
+ const verifyAssignedTo = expectedAssignedTo !== null;
6595
6756
  const expectedCreatedBy = typeof verification.expectedCreatedBy === "string" && verification.expectedCreatedBy.trim() ? verification.expectedCreatedBy : null;
6596
6757
  if (expectedCreatedBy !== null) {
6597
6758
  await requireTaskCreatorCapability(client);
@@ -6604,15 +6765,35 @@ async function cloudCreateTask(client, input, verification = {}) {
6604
6765
  throw new Error(`REMOTE_API_INCOMPATIBLE: configured Todos authority ${remoteAuthorityBase(client)} returned a task create ` + "response without a stored task id; no success row or local SQLite fallback is permitted");
6605
6766
  }
6606
6767
  const persisted = await cloudGetTask(client, created.id);
6607
- if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || (persisted.plan_id ?? null) !== expectedPlanId || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
6768
+ if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || (persisted.plan_id ?? null) !== expectedPlanId || verifyTaskListId && (persisted.task_list_id ?? null) !== expectedTaskListId || verifyAssignedTo && (persisted.assigned_to ?? null) !== expectedAssignedTo || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
6608
6769
  const creatorDetail = expectedCreatedBy === null ? "" : ` and explicit created_by=${JSON.stringify(expectedCreatedBy)} ` + `(readback ${JSON.stringify(persisted?.created_by ?? null)})`;
6609
- throw new Error(`TASK_CREATE_PERSISTENCE_UNVERIFIED: configured Todos authority ${remoteAuthorityBase(client)} accepted ` + `POST /v1/tasks but authoritative GET /v1/tasks/${encodeURIComponent(created.id)} did not return the same ` + `stored task id, parent_id, and plan_id ` + `(requested plan_id=${JSON.stringify(expectedPlanId)}, readback=${JSON.stringify(persisted?.plan_id ?? null)})` + `${creatorDetail}; no success row or local SQLite fallback is permitted`);
6770
+ throw new Error(`TASK_CREATE_PERSISTENCE_UNVERIFIED: configured Todos authority ${remoteAuthorityBase(client)} accepted ` + `POST /v1/tasks but authoritative GET /v1/tasks/${encodeURIComponent(created.id)} did not return the same ` + `stored task id, parent_id, plan_id, and every explicitly requested routing field ` + `(requested plan_id=${JSON.stringify(expectedPlanId)}, readback=${JSON.stringify(persisted?.plan_id ?? null)}; ` + `${verifyTaskListId ? `requested task_list_id=${JSON.stringify(expectedTaskListId)}, readback=${JSON.stringify(persisted?.task_list_id ?? null)}; ` : ""}` + `${verifyAssignedTo ? `requested assigned_to=${JSON.stringify(expectedAssignedTo)}, readback=${JSON.stringify(persisted?.assigned_to ?? null)}` : ""})` + `${creatorDetail}; no success row or local SQLite fallback is permitted`);
6610
6771
  }
6611
6772
  return persisted;
6612
6773
  }
6613
6774
  async function cloudUpdateTask(client, id, patch) {
6614
6775
  return unwrapTask(await client.transport.patch(`/tasks/${encodeURIComponent(id)}`, patch));
6615
6776
  }
6777
+ async function cloudUpdateTaskVerified(client, id, patch) {
6778
+ const task = await cloudUpdateTask(client, id, patch);
6779
+ if (!task || typeof task !== "object" || typeof task.id !== "string" || task.id !== id) {
6780
+ throw new Error(`REMOTE_API_INCOMPATIBLE: PATCH /v1/tasks/${encodeURIComponent(id)} returned a mismatched task`);
6781
+ }
6782
+ const row = task;
6783
+ if (typeof row["title"] !== "string" || !TASK_STATUSES.includes(row["status"]) || !TASK_PRIORITIES.includes(row["priority"]) || !Number.isSafeInteger(row["version"]) || typeof row["created_at"] !== "string" || typeof row["updated_at"] !== "string" || !Array.isArray(row["tags"]) || !row["metadata"] || typeof row["metadata"] !== "object" || Array.isArray(row["metadata"])) {
6784
+ throw new Error(`REMOTE_API_INCOMPATIBLE: PATCH /v1/tasks/${encodeURIComponent(id)} returned an incomplete task record`);
6785
+ }
6786
+ for (const [field, expected] of Object.entries(patch)) {
6787
+ if (field === "version")
6788
+ continue;
6789
+ const actual = row[field] ?? null;
6790
+ const normalizedExpected = expected ?? null;
6791
+ if (JSON.stringify(actual) !== JSON.stringify(normalizedExpected)) {
6792
+ throw new Error(`REMOTE_API_INCOMPATIBLE: PATCH /v1/tasks/${encodeURIComponent(id)} did not preserve requested field ${field}`);
6793
+ }
6794
+ }
6795
+ return task;
6796
+ }
6616
6797
  async function cloudDeleteTask(client, id) {
6617
6798
  try {
6618
6799
  await client.transport.del(`/tasks/${encodeURIComponent(id)}`);
@@ -6623,6 +6804,129 @@ async function cloudDeleteTask(client, id) {
6623
6804
  throw error;
6624
6805
  }
6625
6806
  }
6807
+ function bulkReceiptError(route, detail) {
6808
+ return new Error(`REMOTE_API_INCOMPATIBLE: ${route} ${detail}; refusing to report a partial or ambiguous bulk mutation`);
6809
+ }
6810
+ async function cloudBulkCreateTasks(client, tasks) {
6811
+ const tempIds = new Set(tasks.flatMap((task) => task.temp_id ? [task.temp_id] : []));
6812
+ const normalizedTasks = [];
6813
+ for (const task of tasks) {
6814
+ const dependsOn = [];
6815
+ for (const reference of task.depends_on ?? []) {
6816
+ dependsOn.push(tempIds.has(reference) ? reference : await cloudResolveTaskRef(client, reference));
6817
+ }
6818
+ if (new Set(dependsOn).size !== dependsOn.length) {
6819
+ throw new Error("BULK_CREATE_DUPLICATE_DEPENDENCY: multiple references resolve to the same dependency; no request was sent");
6820
+ }
6821
+ normalizedTasks.push({ ...task, ...dependsOn.length ? { depends_on: dependsOn } : { depends_on: undefined } });
6822
+ }
6823
+ const raw = await requiredRemoteRoute(client, "/v1/tasks/bulk-create", () => client.transport.post("/tasks/bulk-create", { schema_version: 1, tasks: normalizedTasks }, { retry: false }));
6824
+ const receipt = raw && typeof raw === "object" ? raw["receipt"] : null;
6825
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
6826
+ throw bulkReceiptError("/v1/tasks/bulk-create", "did not return a receipt object");
6827
+ }
6828
+ const value = receipt;
6829
+ if (value["schema_version"] !== 1 || value["atomic"] !== true) {
6830
+ throw bulkReceiptError("/v1/tasks/bulk-create", "did not attest schema_version=1 and atomic=true");
6831
+ }
6832
+ const created = value["created"];
6833
+ const dependencies = value["dependencies"];
6834
+ if (!Array.isArray(created) || created.length !== normalizedTasks.length || !Array.isArray(dependencies)) {
6835
+ throw bulkReceiptError("/v1/tasks/bulk-create", "returned incomplete created/dependency collections");
6836
+ }
6837
+ const createdIds = new Set;
6838
+ const parsedCreated = created.map((item, index) => {
6839
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
6840
+ throw bulkReceiptError("/v1/tasks/bulk-create", `created[${index}] is malformed`);
6841
+ }
6842
+ const row = item;
6843
+ const expectedTemp = normalizedTasks[index].temp_id ?? null;
6844
+ if (row["temp_id"] !== expectedTemp || typeof row["id"] !== "string" || !UUID_RE.test(row["id"]) || typeof row["title"] !== "string" || row["title"] !== normalizedTasks[index].title || row["short_id"] !== null && typeof row["short_id"] !== "string") {
6845
+ throw bulkReceiptError("/v1/tasks/bulk-create", `created[${index}] does not match the requested task`);
6846
+ }
6847
+ if (createdIds.has(row["id"]))
6848
+ throw bulkReceiptError("/v1/tasks/bulk-create", "returned duplicate task ids");
6849
+ createdIds.add(row["id"]);
6850
+ return {
6851
+ temp_id: expectedTemp,
6852
+ id: row["id"],
6853
+ short_id: row["short_id"],
6854
+ title: row["title"]
6855
+ };
6856
+ });
6857
+ const idByTemp = new Map;
6858
+ normalizedTasks.forEach((task, index) => {
6859
+ if (task.temp_id)
6860
+ idByTemp.set(task.temp_id, parsedCreated[index].id);
6861
+ });
6862
+ const expectedEdges = normalizedTasks.flatMap((task, index) => (task.depends_on ?? []).map((reference) => ({
6863
+ task_id: parsedCreated[index].id,
6864
+ depends_on: idByTemp.get(reference) ?? reference
6865
+ })));
6866
+ if (dependencies.length !== expectedEdges.length) {
6867
+ throw bulkReceiptError("/v1/tasks/bulk-create", "returned the wrong dependency-edge count");
6868
+ }
6869
+ const seenEdges = new Set;
6870
+ const parsedDependencies = dependencies.map((item, index) => {
6871
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
6872
+ throw bulkReceiptError("/v1/tasks/bulk-create", `dependencies[${index}] is malformed`);
6873
+ }
6874
+ const row = item;
6875
+ if (typeof row["task_id"] !== "string" || !createdIds.has(row["task_id"]) || typeof row["depends_on"] !== "string" || !row["depends_on"]) {
6876
+ throw bulkReceiptError("/v1/tasks/bulk-create", `dependencies[${index}] is not bound to this batch`);
6877
+ }
6878
+ const key = `${row["task_id"]}\x00${row["depends_on"]}`;
6879
+ if (seenEdges.has(key))
6880
+ throw bulkReceiptError("/v1/tasks/bulk-create", "returned duplicate dependency edges");
6881
+ seenEdges.add(key);
6882
+ return { task_id: row["task_id"], depends_on: row["depends_on"] };
6883
+ });
6884
+ const expectedKeys = expectedEdges.map((edge) => `${edge.task_id}\x00${edge.depends_on}`).sort();
6885
+ const receivedKeys = parsedDependencies.map((edge) => `${edge.task_id}\x00${edge.depends_on}`).sort();
6886
+ if (JSON.stringify(receivedKeys) !== JSON.stringify(expectedKeys)) {
6887
+ throw bulkReceiptError("/v1/tasks/bulk-create", "returned dependency edges that do not match the request");
6888
+ }
6889
+ return { schema_version: 1, atomic: true, created: parsedCreated, dependencies: parsedDependencies };
6890
+ }
6891
+ async function cloudBulkDeleteTasks(client, taskIds, force) {
6892
+ const raw = await requiredRemoteRoute(client, "/v1/tasks/bulk-delete", () => client.transport.post("/tasks/bulk-delete", { schema_version: 1, task_ids: taskIds, force }, { retry: false }));
6893
+ const receipt = raw && typeof raw === "object" ? raw["receipt"] : null;
6894
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
6895
+ throw bulkReceiptError("/v1/tasks/bulk-delete", "did not return a receipt object");
6896
+ }
6897
+ const value = receipt;
6898
+ if (value["schema_version"] !== 1 || value["atomic"] !== true || value["force"] !== force || !Array.isArray(value["results"])) {
6899
+ throw bulkReceiptError("/v1/tasks/bulk-delete", "did not return the requested atomic force receipt");
6900
+ }
6901
+ if (value["results"].length !== taskIds.length) {
6902
+ throw bulkReceiptError("/v1/tasks/bulk-delete", "returned an incomplete result set");
6903
+ }
6904
+ const seenTaskIds = new Set;
6905
+ const results = value["results"].map((item, index) => {
6906
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
6907
+ throw bulkReceiptError("/v1/tasks/bulk-delete", `results[${index}] is malformed`);
6908
+ }
6909
+ const row = item;
6910
+ const outcome = row["outcome"];
6911
+ const taskId = row["task_id"];
6912
+ const reason = row["reason"];
6913
+ if (row["requested_id"] !== taskIds[index] || !["deleted", "skipped", "missing"].includes(String(outcome)) || taskId !== null && (typeof taskId !== "string" || !UUID_RE.test(taskId)) || UUID_RE.test(taskIds[index]) && taskId !== null && taskId.toLowerCase() !== taskIds[index].toLowerCase() || outcome === "deleted" && (typeof taskId !== "string" || reason !== null) || outcome === "skipped" && (typeof taskId !== "string" || reason !== "has_children") || outcome === "missing" && (taskId !== null || reason !== "not_found")) {
6914
+ throw bulkReceiptError("/v1/tasks/bulk-delete", `results[${index}] contradicts the request`);
6915
+ }
6916
+ if (typeof taskId === "string") {
6917
+ if (seenTaskIds.has(taskId))
6918
+ throw bulkReceiptError("/v1/tasks/bulk-delete", "returned duplicate resolved task ids");
6919
+ seenTaskIds.add(taskId);
6920
+ }
6921
+ return {
6922
+ requested_id: taskIds[index],
6923
+ task_id: taskId,
6924
+ outcome,
6925
+ reason
6926
+ };
6927
+ });
6928
+ return { schema_version: 1, atomic: true, force, results };
6929
+ }
6626
6930
  function unwrapTemplate(raw) {
6627
6931
  if (raw && typeof raw === "object" && "template" in raw) {
6628
6932
  return raw.template;
@@ -6864,10 +7168,66 @@ async function cloudScanTaskRows(client, options = {}) {
6864
7168
  }
6865
7169
  return { complete: reason === undefined, ...reason ? { reason } : {}, scanned: rows.length, total, pages, rows };
6866
7170
  }
6867
- async function cloudListAgents(client) {
6868
- const res = await client.list("agents");
6869
- const envelope = res.raw;
6870
- return Array.isArray(envelope?.agents) ? envelope.agents : res.items;
7171
+ async function cloudListAgents(client, options = {}) {
7172
+ const maxAgents = options.max_agents ?? 1e4;
7173
+ if (!Number.isSafeInteger(maxAgents) || maxAgents < 1 || maxAgents > 1e4) {
7174
+ throw new Error("max_agents must be an integer from 1 to 10000");
7175
+ }
7176
+ const pageSize = Math.min(500, maxAgents);
7177
+ const agents = [];
7178
+ const seen = new Set;
7179
+ let total = null;
7180
+ while (total === null || agents.length < total) {
7181
+ const offset = agents.length;
7182
+ const requestedLimit = Math.min(pageSize, maxAgents - offset);
7183
+ if (requestedLimit < 1) {
7184
+ throw new Error(`REMOTE_RESULT_TOO_LARGE: /v1/agents exceeds the bounded limit ${maxAgents}`);
7185
+ }
7186
+ const raw = await requiredRemoteRoute(client, "/v1/agents", () => client.transport.get("/agents", {
7187
+ query: { limit: requestedLimit, offset, include_archived: options.include_archived === true }
7188
+ }));
7189
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
7190
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/agents did not return a bounded roster envelope");
7191
+ }
7192
+ const envelope = raw;
7193
+ const page = envelope["agents"];
7194
+ const pageTotal = envelope["total"];
7195
+ if (!Array.isArray(page) || !Number.isSafeInteger(pageTotal) || pageTotal < 0 || envelope["count"] !== page.length || envelope["limit"] !== requestedLimit || envelope["offset"] !== offset || page.length > requestedLimit || offset + page.length > pageTotal) {
7196
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/agents returned a contradictory bounded page");
7197
+ }
7198
+ if (pageTotal > maxAgents) {
7199
+ throw new Error(`REMOTE_RESULT_TOO_LARGE: /v1/agents reports ${String(pageTotal)} rows above the bounded limit ${maxAgents}`);
7200
+ }
7201
+ if (total !== null && pageTotal !== total) {
7202
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/agents total changed during pagination");
7203
+ }
7204
+ total = pageTotal;
7205
+ const hasMore = offset + page.length < total;
7206
+ if (envelope["has_more"] !== hasMore || envelope["next_offset"] !== (hasMore ? offset + page.length : null) || hasMore && page.length === 0) {
7207
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/agents returned a stalled or contradictory page receipt");
7208
+ }
7209
+ for (const item of page) {
7210
+ const agent = item;
7211
+ if (!agent || typeof agent !== "object" || typeof agent.id !== "string" || !agent.id || typeof agent.name !== "string" || !agent.name || seen.has(agent.id)) {
7212
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/agents returned a malformed or repeated agent");
7213
+ }
7214
+ seen.add(agent.id);
7215
+ agents.push(agent);
7216
+ }
7217
+ }
7218
+ return agents;
7219
+ }
7220
+ async function cloudResolveAgentRef(client, ref) {
7221
+ const normalized = ref.trim().toLowerCase();
7222
+ if (!normalized)
7223
+ throw new Error("Agent reference must not be empty");
7224
+ const matches = (await cloudListAgents(client)).filter((agent) => agent.id.toLowerCase() === normalized || agent.name.toLowerCase() === normalized);
7225
+ if (matches.length === 0)
7226
+ throw new Error(`Agent not found: ${ref}`);
7227
+ if (matches.length > 1 && new Set(matches.map((agent) => agent.id)).size > 1) {
7228
+ throw new Error(`Agent reference is ambiguous: ${ref}`);
7229
+ }
7230
+ return matches[0].id;
6871
7231
  }
6872
7232
  async function cloudListProjects(client) {
6873
7233
  const res = await requiredRemoteRoute(client, "/v1/projects", () => client.list("projects"));
@@ -7191,12 +7551,66 @@ function normalizePlanComment(value) {
7191
7551
  function redactComment(comment) {
7192
7552
  return { ...comment, content: redactEvidenceText(comment.content) };
7193
7553
  }
7554
+ function assertTaskHistoryRows(raw, route) {
7555
+ if (!Array.isArray(raw)) {
7556
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} did not return a task-history array`);
7557
+ }
7558
+ for (let index = 0;index < raw.length; index++) {
7559
+ const entry = raw[index];
7560
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
7561
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} entry ${index} is malformed`);
7562
+ }
7563
+ const row = entry;
7564
+ if (typeof row["id"] !== "string" || !row["id"] || typeof row["task_id"] !== "string" || !row["task_id"] || typeof row["action"] !== "string" || !row["action"] || typeof row["created_at"] !== "string" || !row["created_at"]) {
7565
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} entry ${index} is incomplete`);
7566
+ }
7567
+ }
7568
+ return raw;
7569
+ }
7194
7570
  async function cloudTaskHistory(client, taskId) {
7195
7571
  const raw = await client.transport.get(`/tasks/${encodeURIComponent(taskId)}/history`);
7196
- const envelope = raw ?? {};
7197
- if (Array.isArray(envelope.history))
7198
- return envelope.history;
7199
- return Array.isArray(raw) ? raw : [];
7572
+ if (Array.isArray(raw))
7573
+ return assertTaskHistoryRows(raw, "/v1/tasks/:id/history");
7574
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
7575
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks/:id/history did not return an object");
7576
+ }
7577
+ const envelope = raw;
7578
+ const history = assertTaskHistoryRows(envelope["history"], "/v1/tasks/:id/history");
7579
+ if (envelope["count"] !== undefined && envelope["count"] !== history.length) {
7580
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks/:id/history count contradicts its rows");
7581
+ }
7582
+ if (history.some((entry) => entry.task_id !== taskId)) {
7583
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks/:id/history returned an entry for another task");
7584
+ }
7585
+ return history;
7586
+ }
7587
+ async function cloudTaskHistoryPage(client, taskId, options) {
7588
+ const raw = await requiredRemoteRoute(client, "/v1/tasks/:id/history pagination", () => client.transport.get(`/tasks/${encodeURIComponent(taskId)}/history`, { query: options }));
7589
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
7590
+ throw new Error("REMOTE_API_INCOMPATIBLE: paginated task history did not return an object");
7591
+ }
7592
+ const envelope = raw;
7593
+ const history = assertTaskHistoryRows(envelope["history"], "/v1/tasks/:id/history");
7594
+ const count = envelope["count"];
7595
+ const total = envelope["total"];
7596
+ const limit = envelope["limit"];
7597
+ const offset = envelope["offset"];
7598
+ const order = envelope["order"];
7599
+ const hasMore = envelope["has_more"];
7600
+ const nextOffset = envelope["next_offset"];
7601
+ if (count !== history.length || total === undefined || !Number.isSafeInteger(total) || total < 0 || limit !== options.limit || offset !== options.offset || order !== options.order || typeof hasMore !== "boolean" || nextOffset !== null && (!Number.isSafeInteger(nextOffset) || nextOffset < 0) || history.length > options.limit || history.length > 0 && (options.offset >= total || history.length > total - options.offset) || (hasMore ? nextOffset !== options.offset + history.length : nextOffset !== null) || hasMore !== options.offset + history.length < total || history.some((entry) => entry.task_id !== taskId)) {
7602
+ throw new Error("REMOTE_API_INCOMPATIBLE: paginated task history returned contradictory metadata");
7603
+ }
7604
+ return {
7605
+ history,
7606
+ count: history.length,
7607
+ total,
7608
+ limit: options.limit,
7609
+ offset: options.offset,
7610
+ order: options.order,
7611
+ has_more: hasMore,
7612
+ next_offset: nextOffset
7613
+ };
7200
7614
  }
7201
7615
  async function cloudUpsertTaskByFingerprint(client, input) {
7202
7616
  const raw = await requiredRemoteRoute(client, "/v1/tasks/upsert", () => client.transport.post("/tasks/upsert", input));
@@ -7217,6 +7631,43 @@ async function cloudCountTasks(client, filter = {}) {
7217
7631
  const tasks = await cloudListTasks(client, rest);
7218
7632
  return tasks.length;
7219
7633
  }
7634
+ function isCloudAgentRecord(value) {
7635
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7636
+ }
7637
+ function isNullableString(value) {
7638
+ return value === null || typeof value === "string";
7639
+ }
7640
+ function assertCloudAgentEnvelope(raw, expectedId) {
7641
+ if (!isCloudAgentRecord(raw) || !Object.prototype.hasOwnProperty.call(raw, "agent")) {
7642
+ throw new Error("REMOTE_API_INCOMPATIBLE: GET /v1/agents/:id did not return the required { agent } envelope");
7643
+ }
7644
+ const agent = raw["agent"];
7645
+ if (!isCloudAgentRecord(agent)) {
7646
+ throw new Error("REMOTE_API_INCOMPATIBLE: GET /v1/agents/:id returned a malformed agent object");
7647
+ }
7648
+ if (agent["id"] !== expectedId) {
7649
+ throw new Error("REMOTE_API_INCOMPATIBLE: GET /v1/agents/:id returned an agent with a different immutable ID");
7650
+ }
7651
+ if (typeof agent["name"] !== "string" || !agent["name"] || !isNullableString(agent["description"]) || !isNullableString(agent["role"]) || !isNullableString(agent["title"]) || !isNullableString(agent["level"]) || !Array.isArray(agent["permissions"]) || agent["permissions"].some((value) => typeof value !== "string") || !isNullableString(agent["reports_to"]) || !isNullableString(agent["org_id"]) || !Array.isArray(agent["capabilities"]) || agent["capabilities"].some((value) => typeof value !== "string") || !["active", "archived"].includes(String(agent["status"])) || !isCloudAgentRecord(agent["metadata"]) || typeof agent["created_at"] !== "string" || !agent["created_at"] || typeof agent["last_seen_at"] !== "string" || !agent["last_seen_at"] || !isNullableString(agent["session_id"]) || !isNullableString(agent["working_dir"]) || !isNullableString(agent["active_project_id"])) {
7652
+ throw new Error("REMOTE_API_INCOMPATIBLE: GET /v1/agents/:id returned an incomplete agent record");
7653
+ }
7654
+ for (const optionalString of ["identity_id", "project_id", "runtime_instance_id", "machine_id", "synced_at"]) {
7655
+ if (agent[optionalString] !== undefined && !isNullableString(agent[optionalString])) {
7656
+ throw new Error("REMOTE_API_INCOMPATIBLE: GET /v1/agents/:id returned invalid optional agent fields");
7657
+ }
7658
+ }
7659
+ return agent;
7660
+ }
7661
+ async function cloudGetAgent(client, id) {
7662
+ try {
7663
+ const raw = await client.transport.get(`/agents/${encodeURIComponent(id)}`);
7664
+ return assertCloudAgentEnvelope(raw, id);
7665
+ } catch (error) {
7666
+ if (error && typeof error === "object" && error.status === 404)
7667
+ return null;
7668
+ throw error;
7669
+ }
7670
+ }
7220
7671
  async function cloudRegisterAgent(client, input) {
7221
7672
  const raw = await client.transport.post("/agents", input);
7222
7673
  if (raw && typeof raw === "object" && "agent" in raw) {
@@ -7243,6 +7694,20 @@ async function cloudLinkCommit(client, taskId, input) {
7243
7694
  }
7244
7695
  return raw;
7245
7696
  }
7697
+ async function cloudListTaskCommits(client, taskId) {
7698
+ const route = `/v1/tasks/${encodeURIComponent(taskId)}/commits`;
7699
+ const raw = await requiredRemoteRoute(client, route, () => client.transport.get(`/tasks/${encodeURIComponent(taskId)}/commits`));
7700
+ const envelope = raw ?? {};
7701
+ if (!Array.isArray(envelope.commits)) {
7702
+ if (Array.isArray(raw))
7703
+ return raw;
7704
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} did not return a commits array; refusing an incomplete commit trail`);
7705
+ }
7706
+ if (typeof envelope.count === "number" && envelope.count !== envelope.commits.length) {
7707
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} reported ${envelope.count} commits but returned ${envelope.commits.length}; refusing a partial commit trail`);
7708
+ }
7709
+ return envelope.commits;
7710
+ }
7246
7711
  async function cloudFindCommit(client, sha) {
7247
7712
  const raw = await client.transport.get(`/commits/${encodeURIComponent(sha)}`);
7248
7713
  const env = raw ?? {};
@@ -7631,13 +8096,26 @@ async function cloudTaskStats(client, filter = {}) {
7631
8096
  };
7632
8097
  }
7633
8098
  async function cloudRecentActivity(client, limit = 50) {
8099
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4) {
8100
+ throw new Error("activity limit must be an integer from 1 to 10000");
8101
+ }
7634
8102
  const raw = await client.transport.get("/activity", { query: { limit } });
7635
- const envelope = raw ?? {};
7636
- if (Array.isArray(envelope.activity))
7637
- return envelope.activity;
7638
- if (Array.isArray(envelope.entries))
7639
- return envelope.entries;
7640
- return Array.isArray(raw) ? raw : [];
8103
+ if (Array.isArray(raw)) {
8104
+ const legacy = assertTaskHistoryRows(raw, "/v1/activity");
8105
+ if (legacy.length > limit)
8106
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/activity exceeded its requested bound");
8107
+ return legacy;
8108
+ }
8109
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
8110
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/activity did not return an object");
8111
+ }
8112
+ const envelope = raw;
8113
+ const rows = envelope["activity"] ?? envelope["entries"];
8114
+ const activity = assertTaskHistoryRows(rows, "/v1/activity");
8115
+ if (activity.length > limit || envelope["count"] !== activity.length || envelope["limit"] !== undefined && envelope["limit"] !== limit) {
8116
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/activity count or limit contradicts its rows");
8117
+ }
8118
+ return activity;
7641
8119
  }
7642
8120
  async function cloudListTaskLists(client, projectId) {
7643
8121
  const query = projectId ? { project_id: projectId } : {};
@@ -7768,12 +8246,78 @@ async function cloudClaimNext(client, agentId) {
7768
8246
  const task = unwrapTask(raw);
7769
8247
  return task && task.id ? task : null;
7770
8248
  }
8249
+ function dependencyEnvelopeError(detail) {
8250
+ return new Error(`REMOTE_API_INCOMPATIBLE: /v1/dependencies ${detail}; refusing incomplete dependency analytics`);
8251
+ }
7771
8252
  async function cloudAllDependencies(client) {
7772
- const raw = await client.transport.get("/dependencies");
7773
- const envelope = raw ?? {};
7774
- if (Array.isArray(envelope.dependencies))
7775
- return envelope.dependencies;
7776
- return Array.isArray(raw) ? raw : [];
8253
+ const edges = [];
8254
+ const seen = new Set;
8255
+ let offset = 0;
8256
+ let expectedTotal = null;
8257
+ while (true) {
8258
+ const raw = await requiredRemoteRoute(client, "/v1/dependencies", () => client.transport.get("/dependencies", {
8259
+ query: { limit: CLOUD_DEPENDENCY_PAGE_SIZE, offset }
8260
+ }));
8261
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
8262
+ throw dependencyEnvelopeError("did not return an object envelope");
8263
+ }
8264
+ const envelope = raw;
8265
+ const page = envelope["dependencies"];
8266
+ const count = envelope["count"];
8267
+ const total = envelope["total"];
8268
+ const returnedLimit = envelope["limit"];
8269
+ const returnedOffset = envelope["offset"];
8270
+ const hasMore = envelope["has_more"];
8271
+ const nextOffset = envelope["next_offset"];
8272
+ if (!Array.isArray(page))
8273
+ throw dependencyEnvelopeError("did not return a dependencies array");
8274
+ if (!Number.isSafeInteger(count) || count !== page.length) {
8275
+ throw dependencyEnvelopeError("returned a count that does not match the page");
8276
+ }
8277
+ if (!Number.isSafeInteger(total) || total < 0) {
8278
+ throw dependencyEnvelopeError("did not return a non-negative total");
8279
+ }
8280
+ if (total > CLOUD_DEPENDENCY_LIMIT) {
8281
+ throw new Error(`REMOTE_RESULT_TOO_LARGE: /v1/dependencies reports ${total} edges, above the ` + `${CLOUD_DEPENDENCY_LIMIT}-edge analytics limit; narrow the operation or use a server aggregate`);
8282
+ }
8283
+ if (returnedLimit !== CLOUD_DEPENDENCY_PAGE_SIZE || returnedOffset !== offset) {
8284
+ throw dependencyEnvelopeError("did not honor the requested bounded page");
8285
+ }
8286
+ const computedHasMore = offset + page.length < total;
8287
+ if (typeof hasMore !== "boolean" || hasMore !== computedHasMore) {
8288
+ throw dependencyEnvelopeError("returned inconsistent has_more evidence");
8289
+ }
8290
+ if (hasMore ? nextOffset !== offset + page.length : nextOffset !== null) {
8291
+ throw dependencyEnvelopeError("returned an invalid next_offset");
8292
+ }
8293
+ if (expectedTotal === null)
8294
+ expectedTotal = total;
8295
+ else if (expectedTotal !== total)
8296
+ throw dependencyEnvelopeError("changed total during pagination");
8297
+ if (hasMore && page.length === 0)
8298
+ throw dependencyEnvelopeError("made no pagination progress");
8299
+ for (const value of page) {
8300
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
8301
+ throw dependencyEnvelopeError("returned a non-object edge");
8302
+ }
8303
+ const edge = value;
8304
+ if (typeof edge["task_id"] !== "string" || !edge["task_id"] || typeof edge["depends_on"] !== "string" || !edge["depends_on"]) {
8305
+ throw dependencyEnvelopeError("returned an edge without task_id and depends_on");
8306
+ }
8307
+ const key = `${edge["task_id"]}\x00${edge["depends_on"]}`;
8308
+ if (seen.has(key))
8309
+ throw dependencyEnvelopeError("repeated an edge across pages");
8310
+ seen.add(key);
8311
+ edges.push(value);
8312
+ }
8313
+ if (!hasMore) {
8314
+ if (edges.length !== expectedTotal) {
8315
+ throw dependencyEnvelopeError(`returned ${edges.length} edges for total ${expectedTotal}`);
8316
+ }
8317
+ return edges;
8318
+ }
8319
+ offset = nextOffset;
8320
+ }
7777
8321
  }
7778
8322
  async function cloudGetTasksByIds(client, ids) {
7779
8323
  const unique2 = Array.from(new Set(ids));
@@ -7879,7 +8423,7 @@ async function cloudTimeline(client, options = {}) {
7879
8423
  const limit = options.limit ?? 50;
7880
8424
  return { entries: entries.slice(offset, offset + limit), total, limit, offset };
7881
8425
  }
7882
- var UUID_RE, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, retryCapabilityCache, taskCreatorCapabilityCache, gitRefCapabilityCache, remoteCommandCapabilityCache, REMOTE_REQUEST_TIMEOUT_MS = 1e4, PRIORITY_RANK, listTagsCapabilityCache, PLAN_COMPLETION_PROTECTED_FIELDS, RELATION_HYDRATION_CONCURRENCY = 6;
8426
+ var UUID_RE, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, retryCapabilityCache, taskCreatorCapabilityCache, gitRefCapabilityCache, remoteCommandCapabilityCache, REMOTE_REQUEST_TIMEOUT_MS = 1e4, PRIORITY_RANK, listTagsCapabilityCache, PLAN_COMPLETION_PROTECTED_FIELDS, RELATION_HYDRATION_CONCURRENCY = 6, CLOUD_DEPENDENCY_LIMIT = 1e4, CLOUD_DEPENDENCY_PAGE_SIZE = 500;
7883
8427
  var init_cloud_router = __esm(() => {
7884
8428
  init_types();
7885
8429
  init_redaction();
@@ -7920,12 +8464,6 @@ var init_cloud_router = __esm(() => {
7920
8464
  });
7921
8465
 
7922
8466
  // src/cli/stage-a.ts
7923
- function todosLocalModeNotice(reason = "local-opt-in") {
7924
- if (reason === "local-only-command") {
7925
- return "todos: LOCAL mode \u2014 this command only ever runs against the on-box SQLite store, so this run " + "does not reach the hosted fleet even though a Todos authority is configured.";
7926
- }
7927
- return "todos: LOCAL mode \u2014 using the on-box SQLite store, not the hosted fleet " + "(HASNA_TODOS_LOCAL is set). Unset it, and provide a credential via the Keychain item " + "hasna.credentials.todos.api-key, ~/.hasna/todos/config/credentials, or HASNA_TODOS_API_KEY, " + "to work against https://api.hasna.com/todos.";
7928
- }
7929
8467
  function announceTodosCliLocalMode(authority, write = (line) => process.stderr.write(`${line}
7930
8468
  `)) {
7931
8469
  if (authority.route !== "local")
@@ -8266,6 +8804,7 @@ var REGISTERED_CANONICAL_COMMANDS, TODOS_CLI_COMMAND_ALIASES, BUNDLED_STATIC_COM
8266
8804
  var init_stage_a = __esm(() => {
8267
8805
  init_esm();
8268
8806
  init_cloud_router();
8807
+ init_local_opt_in();
8269
8808
  REGISTERED_CANONICAL_COMMANDS = [
8270
8809
  "active",
8271
8810
  "add",
@@ -12403,20 +12942,17 @@ function ensureAgentIdentitySchema(db) {
12403
12942
  SELECT RAISE(ABORT, 'IDENTITY_SOURCE_LINEAGE_IMMUTABLE');
12404
12943
  END;
12405
12944
 
12945
+ -- Drop first so an older definition is replaced rather than silently
12946
+ -- retained (the upgrade path). AGENT_IDENTITY_HISTORY_TRIGGER_DDL then
12947
+ -- re-creates them with CREATE TRIGGER IF NOT EXISTS. The two statements
12948
+ -- do different jobs and both are kept: the DROP is what refreshes a
12949
+ -- stale definition, and the IF NOT EXISTS is what keeps the CREATE safe
12950
+ -- on its own \u2014 so a caller that reaches it without a preceding DROP, in
12951
+ -- its own connection or transaction, gets a no-op instead of
12952
+ -- "trigger ... already exists".
12406
12953
  DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_immutable;
12407
- CREATE TRIGGER trg_agent_identity_mapping_history_immutable
12408
- BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
12409
- ON agent_identity_source_mappings
12410
- BEGIN
12411
- SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
12412
- END;
12413
-
12414
12954
  DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_append_only;
12415
- CREATE TRIGGER trg_agent_identity_mapping_history_append_only
12416
- BEFORE DELETE ON agent_identity_source_mappings
12417
- BEGIN
12418
- SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
12419
- END;
12955
+ ${AGENT_IDENTITY_HISTORY_TRIGGER_DDL}
12420
12956
  `);
12421
12957
  db.run("INSERT OR IGNORE INTO _migrations (id) VALUES (?)", [IDENTITY_MIGRATION_ID]);
12422
12958
  });
@@ -12456,7 +12992,20 @@ function storeAgentAlias(agentId, label, aliasKind, status, db) {
12456
12992
  function recordAgentAlias(agentId, label, db) {
12457
12993
  return storeAgentAlias(agentId, label, "historical", "active", db);
12458
12994
  }
12459
- var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65;
12995
+ var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65, AGENT_IDENTITY_HISTORY_TRIGGER_DDL = `
12996
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_immutable
12997
+ BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
12998
+ ON agent_identity_source_mappings
12999
+ BEGIN
13000
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
13001
+ END;
13002
+
13003
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_append_only
13004
+ BEFORE DELETE ON agent_identity_source_mappings
13005
+ BEGIN
13006
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
13007
+ END;
13008
+ `;
12460
13009
  var init_identity_mapping = __esm(() => {
12461
13010
  init_types();
12462
13011
  init_types();
@@ -12773,15 +13322,18 @@ __export(exports_database, {
12773
13322
  resolvePartialId: () => resolvePartialId,
12774
13323
  resolveAssignedToAliases: () => resolveAssignedToAliases,
12775
13324
  resetDatabase: () => resetDatabase,
13325
+ refuseLocalStore: () => refuseLocalStore,
12776
13326
  now: () => now,
12777
13327
  lowerInClause: () => lowerInClause,
12778
13328
  lockExpiryCutoff: () => lockExpiryCutoff,
12779
13329
  isLockExpired: () => isLockExpired,
13330
+ isLocalStoreRefused: () => isLocalStoreRefused,
12780
13331
  getDatabasePath: () => getDatabasePath,
12781
13332
  getDatabase: () => getDatabase,
12782
13333
  closeDatabase: () => closeDatabase,
12783
13334
  clearExpiredLocks: () => clearExpiredLocks,
12784
13335
  assignedToAliasSet: () => assignedToAliasSet,
13336
+ allowLocalStore: () => allowLocalStore,
12785
13337
  LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
12786
13338
  });
12787
13339
  import { Database } from "bun:sqlite";
@@ -12898,7 +13450,18 @@ function maybeInstallShadowCapture(db) {
12898
13450
  console.error(`[todos] shadow capture install failed: ${error instanceof Error ? error.message : String(error)}`);
12899
13451
  }
12900
13452
  }
13453
+ function refuseLocalStore(message) {
13454
+ localStoreRefusal = message;
13455
+ }
13456
+ function allowLocalStore() {
13457
+ localStoreRefusal = null;
13458
+ }
13459
+ function isLocalStoreRefused() {
13460
+ return localStoreRefusal !== null;
13461
+ }
12901
13462
  function getDatabase(dbPath) {
13463
+ if (localStoreRefusal !== null)
13464
+ throw new Error(localStoreRefusal);
12902
13465
  if (dbPath === undefined && !selectsTodosLocalStore()) {
12903
13466
  throw new Error("API_DATABASE_FALLBACK_FORBIDDEN: this operation must use the shared Todos API; implicit SQLite access is unavailable");
12904
13467
  }
@@ -13039,7 +13602,7 @@ function lowerInClause(column, values, params) {
13039
13602
  params.push(...values.map((v) => v.toLowerCase()));
13040
13603
  return `LOWER(${column}) IN (${values.map(() => "?").join(",")})`;
13041
13604
  }
13042
- var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
13605
+ var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, localStoreRefusal = null, ALLOWED_TABLES;
13043
13606
  var init_database = __esm(() => {
13044
13607
  init_local_opt_in();
13045
13608
  init_schema();
@@ -19142,7 +19705,9 @@ function listTasks(filter = {}, db) {
19142
19705
  params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
19143
19706
  } catch {}
19144
19707
  }
19145
- if (!filter.include_archived) {
19708
+ if (filter.archived_only) {
19709
+ conditions.push("archived_at IS NOT NULL");
19710
+ } else if (!filter.include_archived) {
19146
19711
  conditions.push("archived_at IS NULL");
19147
19712
  }
19148
19713
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
@@ -19155,7 +19720,8 @@ function listTasks(filter = {}, db) {
19155
19720
  params.push(filter.offset);
19156
19721
  }
19157
19722
  }
19158
- const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK2}, created_at DESC, id ASC${limitClause}`).all(...params);
19723
+ const orderBy = filter.archived_only ? "archived_at DESC, id DESC" : `${PRIORITY_RANK2}, created_at DESC, id ASC`;
19724
+ const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${orderBy}${limitClause}`).all(...params);
19159
19725
  return rows.map(rowToTask);
19160
19726
  }
19161
19727
  function getTaskByFingerprint(fingerprint, db) {
@@ -19298,7 +19864,9 @@ function countTasks(filter = {}, db) {
19298
19864
  }
19299
19865
  }
19300
19866
  addMetadataConditions(filter.metadata, conditions, params);
19301
- if (!filter.include_archived) {
19867
+ if (filter.archived_only) {
19868
+ conditions.push("archived_at IS NOT NULL");
19869
+ } else if (!filter.include_archived) {
19302
19870
  conditions.push("archived_at IS NULL");
19303
19871
  }
19304
19872
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
@@ -19435,6 +20003,10 @@ function updateTaskStored(id, input, db) {
19435
20003
  sets.push("completed_at = ?");
19436
20004
  params.push(input.completed_at);
19437
20005
  }
20006
+ if (input.archived_at !== undefined) {
20007
+ sets.push("archived_at = ?");
20008
+ params.push(input.archived_at);
20009
+ }
19438
20010
  if (input.confidence !== undefined) {
19439
20011
  sets.push("confidence = ?");
19440
20012
  params.push(input.confidence);
@@ -19498,6 +20070,8 @@ function updateTaskStored(id, input, db) {
19498
20070
  logTaskChange(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
19499
20071
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
19500
20072
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
20073
+ if (input.archived_at !== undefined && input.archived_at !== task.archived_at)
20074
+ logTaskChange(id, "update", "archived_at", task.archived_at, input.archived_at, agentId, d);
19501
20075
  if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
19502
20076
  logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
19503
20077
  if (input.approved_by !== undefined)
@@ -23102,6 +23676,7 @@ __export(exports_agents, {
23102
23676
  registerAgent: () => registerAgent,
23103
23677
  normalizeGeneratedAgentNames: () => normalizeGeneratedAgentNames,
23104
23678
  matchCapabilities: () => matchCapabilities,
23679
+ listAgentsPage: () => listAgentsPage,
23105
23680
  listAgents: () => listAgents,
23106
23681
  isAgentConflict: () => isAgentConflict,
23107
23682
  getOrgChart: () => getOrgChart,
@@ -23322,6 +23897,19 @@ function listAgents(opts, db) {
23322
23897
  }
23323
23898
  return d.query("SELECT * FROM agents WHERE status = 'active' ORDER BY name").all().map(rowToAgent);
23324
23899
  }
23900
+ function listAgentsPage(options, db) {
23901
+ if (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 500) {
23902
+ throw new Error("Agent page limit must be an integer from 1 to 500");
23903
+ }
23904
+ if (!Number.isSafeInteger(options.offset) || options.offset < 0) {
23905
+ throw new Error("Agent page offset must be a non-negative integer");
23906
+ }
23907
+ const d = db || getDatabase();
23908
+ const where = options.include_archived ? "" : "WHERE status = 'active'";
23909
+ const total = d.query(`SELECT COUNT(*) AS total FROM agents ${where}`).get().total;
23910
+ const agents = d.query(`SELECT * FROM agents ${where} ORDER BY LOWER(name), id LIMIT ? OFFSET ?`).all(options.limit, options.offset).map(rowToAgent);
23911
+ return { agents, total };
23912
+ }
23325
23913
  function updateAgentActivity(id, db) {
23326
23914
  const d = db || getDatabase();
23327
23915
  d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), id]);
@@ -26271,6 +26859,234 @@ async function updateSharedTemplate(client, id, patch) {
26271
26859
  return raw.template;
26272
26860
  }
26273
26861
 
26862
+ // src/cli/template-remote.ts
26863
+ function requireTemplateClient() {
26864
+ assertTemplateApiEnvironment(process.env);
26865
+ const client = getTodosCloudClient();
26866
+ if (!client)
26867
+ throw new Error("Templates require HASNA_TODOS_API_URL and HASNA_TODOS_API_KEY, or saved account credentials");
26868
+ return client;
26869
+ }
26870
+ async function resolveRemoteTemplate(client, ref) {
26871
+ if (!ref.trim())
26872
+ throw new Error("Blank template ID prefix");
26873
+ const direct = await cloudGetTemplate(client, ref);
26874
+ if (direct?.id === ref)
26875
+ return direct;
26876
+ const rows = await listSharedTemplates(client);
26877
+ const exact = rows.find((row) => row.id === ref);
26878
+ const matches = exact ? [exact] : rows.filter((row) => row.id.startsWith(ref));
26879
+ if (!ref.trim() || matches.length > 1)
26880
+ throw new Error("Ambiguous or blank template ID prefix");
26881
+ return matches[0] ? cloudGetTemplate(client, matches[0].id) : null;
26882
+ }
26883
+ async function preflightTemplate(client, template, variables, seen = new Set) {
26884
+ if (seen.has(template.id) || seen.size >= 32)
26885
+ throw new Error("Circular or excessively deep template include graph");
26886
+ seen.add(template.id);
26887
+ try {
26888
+ const resolved = resolveTemplateVariables(template.variables ?? [], variables);
26889
+ for (const step of template.tasks) {
26890
+ if (step.include_template_id) {
26891
+ const included = await resolveRemoteTemplate(client, step.include_template_id);
26892
+ if (!included)
26893
+ throw new Error("Included template not found");
26894
+ await preflightTemplate(client, included, resolved, seen);
26895
+ } else if (step.condition)
26896
+ evaluateTemplateCondition(step.condition, resolved);
26897
+ }
26898
+ } finally {
26899
+ seen.delete(template.id);
26900
+ }
26901
+ }
26902
+ function normalizeTemplateDescription(value) {
26903
+ return value || null;
26904
+ }
26905
+ function previewRemoteTemplate(template, variables) {
26906
+ const resolved = resolveTemplateVariables(template.variables ?? [], variables);
26907
+ const renderDescription = (value) => {
26908
+ if (!value)
26909
+ return null;
26910
+ return substituteTemplateVariables(value, resolved) || null;
26911
+ };
26912
+ if (template.tasks.length === 0) {
26913
+ return {
26914
+ template_id: template.id,
26915
+ template_name: template.name,
26916
+ description: normalizeTemplateDescription(template.description),
26917
+ variables: template.variables,
26918
+ resolved_variables: resolved,
26919
+ tasks: [
26920
+ {
26921
+ position: 0,
26922
+ title: substituteTemplateVariables(template.title_pattern, resolved),
26923
+ description: renderDescription(template.description),
26924
+ priority: template.priority,
26925
+ tags: template.tags,
26926
+ task_type: null,
26927
+ depends_on_positions: []
26928
+ }
26929
+ ]
26930
+ };
26931
+ }
26932
+ return {
26933
+ template_id: template.id,
26934
+ template_name: template.name,
26935
+ description: normalizeTemplateDescription(template.description),
26936
+ variables: template.variables,
26937
+ resolved_variables: resolved,
26938
+ tasks: template.tasks.filter((step) => !step.condition || evaluateTemplateCondition(step.condition, resolved)).map((step) => ({
26939
+ position: step.position,
26940
+ title: substituteTemplateVariables(step.title_pattern, resolved),
26941
+ description: renderDescription(step.description),
26942
+ priority: step.priority,
26943
+ tags: step.tags,
26944
+ task_type: step.task_type,
26945
+ depends_on_positions: step.depends_on_positions
26946
+ }))
26947
+ };
26948
+ }
26949
+ function exportRemoteTemplate(template) {
26950
+ return {
26951
+ name: template.name,
26952
+ title_pattern: template.title_pattern,
26953
+ description: normalizeTemplateDescription(template.description),
26954
+ priority: template.priority,
26955
+ tags: template.tags,
26956
+ variables: template.variables,
26957
+ project_id: template.project_id,
26958
+ plan_id: template.plan_id,
26959
+ metadata: template.metadata,
26960
+ tasks: template.tasks.map((step) => ({
26961
+ position: step.position,
26962
+ title_pattern: step.title_pattern,
26963
+ description: normalizeTemplateDescription(step.description),
26964
+ priority: step.priority,
26965
+ tags: step.tags,
26966
+ task_type: step.task_type,
26967
+ condition: step.condition,
26968
+ include_template_id: step.include_template_id,
26969
+ depends_on_positions: step.depends_on_positions,
26970
+ metadata: step.metadata
26971
+ }))
26972
+ };
26973
+ }
26974
+ async function createRemoteTemplateTasks(cloud, template, projectId, variables, options = {}, visited = new Set, progress = {
26975
+ tasks: [],
26976
+ dependencies: [],
26977
+ pending: null
26978
+ }) {
26979
+ if (visited.size === 0)
26980
+ await preflightTemplate(cloud, template, variables);
26981
+ const { actorAgentId, assignedTo, taskListId, overrides } = options;
26982
+ if (visited.has(template.id)) {
26983
+ throw new Error(`Circular template reference detected: ${template.id}`);
26984
+ }
26985
+ visited.add(template.id);
26986
+ try {
26987
+ const resolved = resolveTemplateVariables(template.variables ?? [], variables);
26988
+ const render = (value) => value === null || value === undefined ? value : substituteTemplateVariables(value, resolved);
26989
+ if (template.tasks.length === 0) {
26990
+ progress.pending = "task-create";
26991
+ const task = await cloudCreateTask(cloud, {
26992
+ title: render(overrides?.title || template.title_pattern),
26993
+ ...render(overrides?.description ?? template.description) ? {
26994
+ description: render(overrides?.description ?? template.description)
26995
+ } : {},
26996
+ priority: overrides?.priority ?? template.priority,
26997
+ tags: template.tags,
26998
+ ...projectId ? { project_id: projectId } : {},
26999
+ ...taskListId ? { task_list_id: taskListId } : {},
27000
+ ...template.plan_id ? { plan_id: template.plan_id } : {},
27001
+ ...actorAgentId ? { agent_id: actorAgentId } : {},
27002
+ ...assignedTo ? { assigned_to: assignedTo } : {},
27003
+ ...Object.keys(template.metadata ?? {}).length > 0 ? { metadata: template.metadata } : {}
27004
+ });
27005
+ progress.tasks.push(task);
27006
+ progress.pending = null;
27007
+ return { tasks: [task] };
27008
+ }
27009
+ const created = [];
27010
+ const positionToTaskId = new Map;
27011
+ const skippedPositions = new Set;
27012
+ for (const step of template.tasks) {
27013
+ if (step.include_template_id) {
27014
+ const included = await resolveRemoteTemplate(cloud, step.include_template_id);
27015
+ if (!included)
27016
+ throw new Error(`Included template not found: ${step.include_template_id}`);
27017
+ const result = await createRemoteTemplateTasks(cloud, included, projectId, resolved, { ...options, overrides: undefined }, visited, progress);
27018
+ created.push(...result.tasks);
27019
+ if (result.tasks.length > 0)
27020
+ positionToTaskId.set(step.position, result.tasks[0].id);
27021
+ else
27022
+ skippedPositions.add(step.position);
27023
+ continue;
27024
+ }
27025
+ if (step.condition && !evaluateTemplateCondition(step.condition, resolved)) {
27026
+ skippedPositions.add(step.position);
27027
+ continue;
27028
+ }
27029
+ progress.pending = "task-create";
27030
+ const task = await cloudCreateTask(cloud, {
27031
+ title: render(step.title_pattern),
27032
+ ...render(step.description) ? { description: render(step.description) } : {},
27033
+ priority: step.priority,
27034
+ tags: step.tags,
27035
+ ...step.task_type ? { task_type: step.task_type } : {},
27036
+ ...projectId ? { project_id: projectId } : {},
27037
+ ...taskListId ? { task_list_id: taskListId } : {},
27038
+ ...template.plan_id ? { plan_id: template.plan_id } : {},
27039
+ ...actorAgentId ? { agent_id: actorAgentId } : {},
27040
+ ...assignedTo ? { assigned_to: assignedTo } : {},
27041
+ ...Object.keys(step.metadata ?? {}).length > 0 ? { metadata: step.metadata } : {}
27042
+ });
27043
+ progress.tasks.push(task);
27044
+ progress.pending = null;
27045
+ created.push(task);
27046
+ positionToTaskId.set(step.position, task.id);
27047
+ }
27048
+ for (const step of template.tasks) {
27049
+ if (skippedPositions.has(step.position) || step.include_template_id)
27050
+ continue;
27051
+ const taskId = positionToTaskId.get(step.position);
27052
+ if (!taskId)
27053
+ continue;
27054
+ for (const dependencyPosition of step.depends_on_positions) {
27055
+ if (skippedPositions.has(dependencyPosition))
27056
+ continue;
27057
+ const dependencyId = positionToTaskId.get(dependencyPosition);
27058
+ if (dependencyId) {
27059
+ progress.pending = "dependency-add";
27060
+ await cloudAddDependency(cloud, taskId, dependencyId);
27061
+ progress.dependencies.push({
27062
+ task_id: taskId,
27063
+ depends_on: dependencyId
27064
+ });
27065
+ progress.pending = null;
27066
+ }
27067
+ }
27068
+ }
27069
+ return { tasks: created };
27070
+ } catch (error) {
27071
+ if (error instanceof TemplateApplyError)
27072
+ throw error;
27073
+ throw new TemplateApplyError(progress);
27074
+ } finally {
27075
+ visited.delete(template.id);
27076
+ }
27077
+ }
27078
+ var TemplateApplyError;
27079
+ var init_template_remote = __esm(() => {
27080
+ init_cloud_router();
27081
+ TemplateApplyError = class TemplateApplyError extends Error {
27082
+ progress;
27083
+ constructor(progress) {
27084
+ super("Template application did not finish; inspect confirmed tasks before retrying");
27085
+ this.progress = progress;
27086
+ }
27087
+ };
27088
+ });
27089
+
26274
27090
  // src/lib/builtin-template-library.ts
26275
27091
  var exports_builtin_template_library = {};
26276
27092
  __export(exports_builtin_template_library, {
@@ -27098,26 +27914,6 @@ var init_builtin_template_library = __esm(() => {
27098
27914
 
27099
27915
  // src/cli/commands/template-commands.ts
27100
27916
  import chalk4 from "chalk";
27101
- function requireTemplateClient() {
27102
- assertTemplateApiEnvironment(process.env);
27103
- const client = getTodosCloudClient();
27104
- if (!client)
27105
- throw new Error("Templates require HASNA_TODOS_API_URL and HASNA_TODOS_API_KEY, or saved account credentials");
27106
- return client;
27107
- }
27108
- async function cloudGetTemplate2(client, ref) {
27109
- if (!ref.trim())
27110
- throw new Error("Blank template ID prefix");
27111
- const direct = await cloudGetTemplate(client, ref);
27112
- if (direct?.id === ref)
27113
- return direct;
27114
- const rows = await listSharedTemplates(client);
27115
- const exact = rows.find((row) => row.id === ref);
27116
- const matches = exact ? [exact] : rows.filter((row) => row.id.startsWith(ref));
27117
- if (!ref.trim() || matches.length > 1)
27118
- throw new Error("Ambiguous or blank template ID prefix");
27119
- return matches[0] ? cloudGetTemplate(client, matches[0].id) : null;
27120
- }
27121
27917
  function reportTemplateError(error, jsonMode) {
27122
27918
  if (!(error instanceof TemplateApplyError)) {
27123
27919
  handleError(error);
@@ -27133,196 +27929,6 @@ function reportTemplateError(error, jsonMode) {
27133
27929
  }, jsonMode);
27134
27930
  process.exitCode = 1;
27135
27931
  }
27136
- async function preflightTemplate(client, template, variables, seen = new Set) {
27137
- if (seen.has(template.id) || seen.size >= 32)
27138
- throw new Error("Circular or excessively deep template include graph");
27139
- seen.add(template.id);
27140
- try {
27141
- const resolved = resolveTemplateVariables(template.variables ?? [], variables);
27142
- for (const step of template.tasks) {
27143
- if (step.include_template_id) {
27144
- const included = await cloudGetTemplate2(client, step.include_template_id);
27145
- if (!included)
27146
- throw new Error("Included template not found");
27147
- await preflightTemplate(client, included, resolved, seen);
27148
- } else if (step.condition)
27149
- evaluateTemplateCondition(step.condition, resolved);
27150
- }
27151
- } finally {
27152
- seen.delete(template.id);
27153
- }
27154
- }
27155
- function normalizeTemplateDescription(value) {
27156
- return value || null;
27157
- }
27158
- function previewRemoteTemplate(template, variables) {
27159
- const resolved = resolveTemplateVariables(template.variables ?? [], variables);
27160
- const renderDescription = (value) => {
27161
- if (!value)
27162
- return null;
27163
- return substituteTemplateVariables(value, resolved) || null;
27164
- };
27165
- if (template.tasks.length === 0) {
27166
- return {
27167
- template_id: template.id,
27168
- template_name: template.name,
27169
- description: normalizeTemplateDescription(template.description),
27170
- variables: template.variables,
27171
- resolved_variables: resolved,
27172
- tasks: [
27173
- {
27174
- position: 0,
27175
- title: substituteTemplateVariables(template.title_pattern, resolved),
27176
- description: renderDescription(template.description),
27177
- priority: template.priority,
27178
- tags: template.tags,
27179
- task_type: null,
27180
- depends_on_positions: []
27181
- }
27182
- ]
27183
- };
27184
- }
27185
- return {
27186
- template_id: template.id,
27187
- template_name: template.name,
27188
- description: normalizeTemplateDescription(template.description),
27189
- variables: template.variables,
27190
- resolved_variables: resolved,
27191
- tasks: template.tasks.filter((step) => !step.condition || evaluateTemplateCondition(step.condition, resolved)).map((step) => ({
27192
- position: step.position,
27193
- title: substituteTemplateVariables(step.title_pattern, resolved),
27194
- description: renderDescription(step.description),
27195
- priority: step.priority,
27196
- tags: step.tags,
27197
- task_type: step.task_type,
27198
- depends_on_positions: step.depends_on_positions
27199
- }))
27200
- };
27201
- }
27202
- function exportRemoteTemplate(template) {
27203
- return {
27204
- name: template.name,
27205
- title_pattern: template.title_pattern,
27206
- description: normalizeTemplateDescription(template.description),
27207
- priority: template.priority,
27208
- tags: template.tags,
27209
- variables: template.variables,
27210
- project_id: template.project_id,
27211
- plan_id: template.plan_id,
27212
- metadata: template.metadata,
27213
- tasks: template.tasks.map((step) => ({
27214
- position: step.position,
27215
- title_pattern: step.title_pattern,
27216
- description: normalizeTemplateDescription(step.description),
27217
- priority: step.priority,
27218
- tags: step.tags,
27219
- task_type: step.task_type,
27220
- condition: step.condition,
27221
- include_template_id: step.include_template_id,
27222
- depends_on_positions: step.depends_on_positions,
27223
- metadata: step.metadata
27224
- }))
27225
- };
27226
- }
27227
- async function createRemoteTemplateTasks(cloud, template, projectId, variables, agentId, overrides, visited = new Set, progress = {
27228
- tasks: [],
27229
- dependencies: [],
27230
- pending: null
27231
- }) {
27232
- if (visited.size === 0)
27233
- await preflightTemplate(cloud, template, variables);
27234
- if (visited.has(template.id)) {
27235
- throw new Error(`Circular template reference detected: ${template.id}`);
27236
- }
27237
- visited.add(template.id);
27238
- try {
27239
- const resolved = resolveTemplateVariables(template.variables ?? [], variables);
27240
- const render = (value) => value === null || value === undefined ? value : substituteTemplateVariables(value, resolved);
27241
- if (template.tasks.length === 0) {
27242
- progress.pending = "task-create";
27243
- const task = await cloudCreateTask(cloud, {
27244
- title: render(overrides?.title || template.title_pattern),
27245
- ...render(overrides?.description ?? template.description) ? {
27246
- description: render(overrides?.description ?? template.description)
27247
- } : {},
27248
- priority: overrides?.priority ?? template.priority,
27249
- tags: template.tags,
27250
- ...projectId ? { project_id: projectId } : {},
27251
- ...template.plan_id ? { plan_id: template.plan_id } : {},
27252
- ...agentId ? { agent_id: agentId } : {},
27253
- ...Object.keys(template.metadata ?? {}).length > 0 ? { metadata: template.metadata } : {}
27254
- });
27255
- progress.tasks.push(task);
27256
- progress.pending = null;
27257
- return { tasks: [task] };
27258
- }
27259
- const created = [];
27260
- const positionToTaskId = new Map;
27261
- const skippedPositions = new Set;
27262
- for (const step of template.tasks) {
27263
- if (step.include_template_id) {
27264
- const included = await cloudGetTemplate2(cloud, step.include_template_id);
27265
- if (!included)
27266
- throw new Error(`Included template not found: ${step.include_template_id}`);
27267
- const result = await createRemoteTemplateTasks(cloud, included, projectId, resolved, agentId, undefined, visited, progress);
27268
- created.push(...result.tasks);
27269
- if (result.tasks.length > 0)
27270
- positionToTaskId.set(step.position, result.tasks[0].id);
27271
- else
27272
- skippedPositions.add(step.position);
27273
- continue;
27274
- }
27275
- if (step.condition && !evaluateTemplateCondition(step.condition, resolved)) {
27276
- skippedPositions.add(step.position);
27277
- continue;
27278
- }
27279
- progress.pending = "task-create";
27280
- const task = await cloudCreateTask(cloud, {
27281
- title: render(step.title_pattern),
27282
- ...render(step.description) ? { description: render(step.description) } : {},
27283
- priority: step.priority,
27284
- tags: step.tags,
27285
- ...step.task_type ? { task_type: step.task_type } : {},
27286
- ...projectId ? { project_id: projectId } : {},
27287
- ...template.plan_id ? { plan_id: template.plan_id } : {},
27288
- ...agentId ? { agent_id: agentId } : {},
27289
- ...Object.keys(step.metadata ?? {}).length > 0 ? { metadata: step.metadata } : {}
27290
- });
27291
- progress.tasks.push(task);
27292
- progress.pending = null;
27293
- created.push(task);
27294
- positionToTaskId.set(step.position, task.id);
27295
- }
27296
- for (const step of template.tasks) {
27297
- if (skippedPositions.has(step.position) || step.include_template_id)
27298
- continue;
27299
- const taskId = positionToTaskId.get(step.position);
27300
- if (!taskId)
27301
- continue;
27302
- for (const dependencyPosition of step.depends_on_positions) {
27303
- if (skippedPositions.has(dependencyPosition))
27304
- continue;
27305
- const dependencyId = positionToTaskId.get(dependencyPosition);
27306
- if (dependencyId) {
27307
- progress.pending = "dependency-add";
27308
- await cloudAddDependency(cloud, taskId, dependencyId);
27309
- progress.dependencies.push({
27310
- task_id: taskId,
27311
- depends_on: dependencyId
27312
- });
27313
- progress.pending = null;
27314
- }
27315
- }
27316
- }
27317
- return { tasks: created };
27318
- } catch (error) {
27319
- if (error instanceof TemplateApplyError)
27320
- throw error;
27321
- throw new TemplateApplyError(progress);
27322
- } finally {
27323
- visited.delete(template.id);
27324
- }
27325
- }
27326
27932
  function registerTemplateCommands(program2) {
27327
27933
  program2.command("templates").description("List and manage task templates").option("--add <name>", "Create a template").option("--title <pattern>", "Title pattern (with --add)").option("-d, --description <text>", "Default description").option("-p, --priority <level>", "Default priority").option("-t, --tags <tags>", "Default tags (comma-separated)").option("--delete <id>", "Delete a template").option("--update <id>", "Update a template").option("--use <id>", "Create a task from a template").option("--var <vars...>", "Variable substitutions: key=value (e.g. --var feature=login)").action(async (opts) => {
27328
27934
  const globalOpts = program2.opts();
@@ -27350,7 +27956,7 @@ function registerTemplateCommands(program2) {
27350
27956
  return;
27351
27957
  }
27352
27958
  if (opts.delete) {
27353
- const prior = await cloudGetTemplate2(cloud, opts.delete);
27959
+ const prior = await resolveRemoteTemplate(cloud, opts.delete);
27354
27960
  const deleted = prior ? await cloudDeleteTemplate(cloud, prior.id) : false;
27355
27961
  if (globalOpts.json) {
27356
27962
  output({ deleted }, true);
@@ -27374,7 +27980,7 @@ function registerTemplateCommands(program2) {
27374
27980
  if (Object.keys(updates).length === 0) {
27375
27981
  handleError(new Error("Provide --title, --description, --priority, or --tags with --update"));
27376
27982
  }
27377
- const prior = await cloudGetTemplate2(cloud, opts.update);
27983
+ const prior = await resolveRemoteTemplate(cloud, opts.update);
27378
27984
  if (!prior)
27379
27985
  handleError(new Error("Template not found."));
27380
27986
  const updated = await updateSharedTemplate(cloud, prior.id, {
@@ -27400,15 +28006,18 @@ function registerTemplateCommands(program2) {
27400
28006
  }
27401
28007
  variables[value.slice(0, separator)] = value.slice(separator + 1);
27402
28008
  }
27403
- const template = await cloudGetTemplate2(cloud, opts.use);
28009
+ const template = await resolveRemoteTemplate(cloud, opts.use);
27404
28010
  if (!template) {
27405
28011
  handleError(new Error("Template not found."));
27406
28012
  }
27407
28013
  const targetProjectId = template.project_id ?? projectId;
27408
- const { tasks: created } = await createRemoteTemplateTasks(cloud, template, targetProjectId, variables, globalOpts.agent, {
27409
- title: opts.title,
27410
- description: opts.description,
27411
- priority: opts.priority
28014
+ const { tasks: created } = await createRemoteTemplateTasks(cloud, template, targetProjectId, variables, {
28015
+ actorAgentId: globalOpts.agent,
28016
+ overrides: {
28017
+ title: opts.title,
28018
+ description: opts.description,
28019
+ priority: opts.priority
28020
+ }
27412
28021
  });
27413
28022
  if (globalOpts.json) {
27414
28023
  output(created, true);
@@ -27515,7 +28124,7 @@ function registerTemplateCommands(program2) {
27515
28124
  const cloud = requireTemplateClient();
27516
28125
  let result;
27517
28126
  if (cloud) {
27518
- const template = await cloudGetTemplate2(cloud, id);
28127
+ const template = await resolveRemoteTemplate(cloud, id);
27519
28128
  if (!template) {
27520
28129
  handleError(new Error("Template not found."));
27521
28130
  }
@@ -27548,7 +28157,7 @@ function registerTemplateCommands(program2) {
27548
28157
  program2.command("template-export <id>").alias("templates-export").description("Export a template as JSON to stdout").action(async (id) => {
27549
28158
  try {
27550
28159
  const cloud = requireTemplateClient();
27551
- const template = cloud ? await cloudGetTemplate2(cloud, id) : null;
28160
+ const template = cloud ? await resolveRemoteTemplate(cloud, id) : null;
27552
28161
  if (cloud && !template) {
27553
28162
  handleError(new Error("Template not found."));
27554
28163
  }
@@ -27583,7 +28192,7 @@ function registerTemplateCommands(program2) {
27583
28192
  const globalOpts = program2.opts();
27584
28193
  const cloud = requireTemplateClient();
27585
28194
  try {
27586
- const template = await cloudGetTemplate2(cloud, id);
28195
+ const template = await resolveRemoteTemplate(cloud, id);
27587
28196
  if (!template) {
27588
28197
  handleError(new Error("Template not found."));
27589
28198
  }
@@ -27614,17 +28223,10 @@ function registerTemplateCommands(program2) {
27614
28223
  }
27615
28224
  });
27616
28225
  }
27617
- var TemplateApplyError;
27618
28226
  var init_template_commands = __esm(() => {
27619
28227
  init_helpers();
27620
28228
  init_cloud_router();
27621
- TemplateApplyError = class TemplateApplyError extends Error {
27622
- progress;
27623
- constructor(progress) {
27624
- super("Template application did not finish; inspect confirmed tasks before retrying");
27625
- this.progress = progress;
27626
- }
27627
- };
28229
+ init_template_remote();
27628
28230
  });
27629
28231
 
27630
28232
  // src/cli/commands/plan-template-commands.ts
@@ -30075,6 +30677,102 @@ var init_sqlite_snapshot = __esm(() => {
30075
30677
  });
30076
30678
 
30077
30679
  // src/storage/local-sqlite.ts
30680
+ var exports_local_sqlite = {};
30681
+ __export(exports_local_sqlite, {
30682
+ createLocalSqliteTodosStorageAdapter: () => createLocalSqliteTodosStorageAdapter
30683
+ });
30684
+ function bulkCreateAtomicSqlite(db, inputs) {
30685
+ return db.transaction(() => {
30686
+ const tempIds = new Map;
30687
+ const created = [];
30688
+ const dependencies = [];
30689
+ for (const input of inputs) {
30690
+ const { temp_id, depends_on: _dependsOn, ...taskInput } = input;
30691
+ const task = createTask(taskInput, db);
30692
+ if (temp_id)
30693
+ tempIds.set(temp_id, task.id);
30694
+ created.push({ temp_id: temp_id ?? null, id: task.id, short_id: task.short_id, title: task.title });
30695
+ }
30696
+ for (let index = 0;index < inputs.length; index++) {
30697
+ const input = inputs[index];
30698
+ const taskId = created[index].id;
30699
+ const seenDependencies = new Set;
30700
+ for (const reference of input.depends_on ?? []) {
30701
+ const dependencyId = tempIds.get(reference) ?? resolveTaskRefLocal(db, reference)?.id;
30702
+ if (!dependencyId)
30703
+ throw new TaskNotFoundError(reference);
30704
+ if (seenDependencies.has(dependencyId)) {
30705
+ throw new ResourceConflictError("BULK_CREATE_DUPLICATE_DEPENDENCY", `Multiple references resolve to dependency ${dependencyId}`);
30706
+ }
30707
+ seenDependencies.add(dependencyId);
30708
+ addDependency(taskId, dependencyId, db);
30709
+ dependencies.push({ task_id: taskId, depends_on: dependencyId });
30710
+ }
30711
+ }
30712
+ return { schema_version: 1, atomic: true, created, dependencies };
30713
+ })();
30714
+ }
30715
+ function deleteTaskHierarchySqlite(db, rootId) {
30716
+ const rows = db.query(`WITH RECURSIVE tree(id, depth) AS (
30717
+ SELECT id, 0 FROM tasks WHERE id = ?
30718
+ UNION ALL
30719
+ SELECT child.id, tree.depth + 1 FROM tasks child JOIN tree ON child.parent_id = tree.id
30720
+ ) SELECT id FROM tree ORDER BY depth DESC, id`).all(rootId);
30721
+ if (rows.length === 0)
30722
+ throw new TaskNotFoundError(rootId);
30723
+ for (const row of rows) {
30724
+ if (!deleteTask(row.id, db))
30725
+ throw new Error(`Atomic hierarchy delete lost task ${row.id}`);
30726
+ }
30727
+ }
30728
+ function bulkDeleteAtomicSqlite(db, ids, force) {
30729
+ return db.transaction(() => {
30730
+ const resolved = ids.map((reference) => ({ reference, task: resolveTaskRefLocal(db, reference) }));
30731
+ const seen = new Set;
30732
+ for (const item of resolved) {
30733
+ if (!item.task)
30734
+ continue;
30735
+ if (seen.has(item.task.id)) {
30736
+ throw new ResourceConflictError("BULK_DELETE_DUPLICATE_TASK", `Multiple references resolve to task ${item.task.id}`);
30737
+ }
30738
+ seen.add(item.task.id);
30739
+ }
30740
+ const childState = new Map;
30741
+ for (const item of resolved) {
30742
+ if (!item.task)
30743
+ continue;
30744
+ childState.set(item.task.id, Boolean(db.query("SELECT id FROM tasks WHERE parent_id = ? LIMIT 1").get(item.task.id)));
30745
+ }
30746
+ const planned = resolved.filter((item) => item.task && (force || !childState.get(item.task.id)));
30747
+ const plannedIds = new Set(planned.map((item) => item.task.id));
30748
+ const roots = planned.filter((item) => {
30749
+ if (!force)
30750
+ return true;
30751
+ let parentId = item.task.parent_id;
30752
+ while (parentId) {
30753
+ if (plannedIds.has(parentId))
30754
+ return false;
30755
+ parentId = getTask(parentId, db)?.parent_id ?? null;
30756
+ }
30757
+ return true;
30758
+ });
30759
+ for (const item of roots)
30760
+ deleteTaskHierarchySqlite(db, item.task.id);
30761
+ return {
30762
+ schema_version: 1,
30763
+ atomic: true,
30764
+ force,
30765
+ results: resolved.map((item) => {
30766
+ if (!item.task)
30767
+ return { requested_id: item.reference, task_id: null, outcome: "missing", reason: "not_found" };
30768
+ if (!force && childState.get(item.task.id)) {
30769
+ return { requested_id: item.reference, task_id: item.task.id, outcome: "skipped", reason: "has_children" };
30770
+ }
30771
+ return { requested_id: item.reference, task_id: item.task.id, outcome: "deleted", reason: null };
30772
+ })
30773
+ };
30774
+ })();
30775
+ }
30078
30776
  function resolveTaskRefLocal(db, ref) {
30079
30777
  const raw = ref.trim().toLowerCase();
30080
30778
  if (!raw)
@@ -30170,6 +30868,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
30170
30868
  },
30171
30869
  handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
30172
30870
  delete: (id) => deleteTask(id, database()),
30871
+ bulkCreateAtomic: (inputs, _context) => bulkCreateAtomicSqlite(database(), inputs),
30872
+ bulkDeleteAtomic: (ids, force, _context) => bulkDeleteAtomicSqlite(database(), ids, force),
30173
30873
  start: (id, agentId) => startTask(id, agentId, database()),
30174
30874
  complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
30175
30875
  fail: (id, agentId, reason, options2) => failTask(id, agentId, reason, options2, database()),
@@ -30178,6 +30878,30 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
30178
30878
  getActiveWork: (filters) => getActiveWork(filters, database()),
30179
30879
  getChangedSince: (since, filters) => getTasksChangedSince(since, filters, database())
30180
30880
  },
30881
+ dependencies: {
30882
+ add: (taskId, dependsOn) => {
30883
+ addDependency(taskId, dependsOn, database());
30884
+ return { task_id: taskId, depends_on: dependsOn };
30885
+ },
30886
+ remove: (taskId, dependsOn) => removeDependency(taskId, dependsOn, database()),
30887
+ list: (taskId) => {
30888
+ const dependencies = getTaskDependencies(taskId, database());
30889
+ const blocks = getTaskDependents(taskId, database());
30890
+ return { dependencies, blocks, blocked_by: blocks };
30891
+ },
30892
+ listPage: ({ limit, offset }) => {
30893
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
30894
+ throw new Error("SQLite dependency page limit must be an integer from 1 to 500");
30895
+ }
30896
+ if (!Number.isSafeInteger(offset) || offset < 0) {
30897
+ throw new Error("SQLite dependency page offset must be a non-negative integer");
30898
+ }
30899
+ const totalRow = database().query("SELECT COUNT(*) AS total FROM task_dependencies").get();
30900
+ const dependencies = database().query("SELECT task_id, depends_on FROM task_dependencies ORDER BY task_id, depends_on LIMIT ? OFFSET ?").all(limit, offset);
30901
+ return { dependencies, total: totalRow.total };
30902
+ },
30903
+ listAll: () => database().query("SELECT task_id, depends_on FROM task_dependencies ORDER BY task_id, depends_on").all()
30904
+ },
30181
30905
  projects: {
30182
30906
  create: (input) => createProject(input, database()),
30183
30907
  get: (id) => getProject(id, database()),
@@ -30221,6 +30945,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
30221
30945
  get: (id) => getAgent(id, database()),
30222
30946
  getByName: (name) => getAgentByName(name, database()),
30223
30947
  list: (options2) => listAgents(options2, database()),
30948
+ listPage: (options2) => listAgentsPage(options2, database()),
30224
30949
  update: (id, input) => updateAgent(id, input, database())
30225
30950
  },
30226
30951
  taskLists: {
@@ -30259,6 +30984,29 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
30259
30984
  return comments;
30260
30985
  },
30261
30986
  getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
30987
+ getTaskHistoryPage: (taskId, options2) => {
30988
+ if (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 500) {
30989
+ throw new Error("Task history limit must be an integer from 1 to 500");
30990
+ }
30991
+ if (!Number.isSafeInteger(options2.offset) || options2.offset < 0) {
30992
+ throw new Error("Task history offset must be a non-negative integer");
30993
+ }
30994
+ const conditions = ["task_id = ?"];
30995
+ const values = [taskId];
30996
+ if (options2.since) {
30997
+ conditions.push("created_at >= ?");
30998
+ values.push(options2.since);
30999
+ }
31000
+ if (options2.until) {
31001
+ conditions.push("created_at <= ?");
31002
+ values.push(options2.until);
31003
+ }
31004
+ const where = conditions.join(" AND ");
31005
+ const total = database().query(`SELECT COUNT(*) AS total FROM task_history WHERE ${where}`).get(...values).total;
31006
+ const direction = options2.order === "asc" ? "ASC" : "DESC";
31007
+ const history = database().query(`SELECT * FROM task_history WHERE ${where} ORDER BY created_at ${direction}, id ${direction} LIMIT ? OFFSET ?`).all(...values, options2.limit, options2.offset);
31008
+ return { history, total };
31009
+ },
30262
31010
  getRecentActivity: (limit) => getRecentActivity(limit, database())
30263
31011
  },
30264
31012
  sync: {
@@ -30281,6 +31029,7 @@ var init_local_sqlite = __esm(() => {
30281
31029
  init_types();
30282
31030
  init_search();
30283
31031
  init_tasks();
31032
+ init_task_graph();
30284
31033
  init_projects();
30285
31034
  init_plans();
30286
31035
  init_plan_comments();
@@ -34755,6 +35504,8 @@ function createPostgresTodosStorageAdapter(options) {
34755
35504
  count: (filter = {}) => store.countTasks(filter),
34756
35505
  update: (id, input, context) => updateTask2(id, input, store, context),
34757
35506
  delete: (id, context) => store.deleteTaskHierarchy(id, context),
35507
+ bulkCreateAtomic: (inputs, context) => bulkCreateTasksAtomic(inputs, store, context),
35508
+ bulkDeleteAtomic: (ids, force, context) => bulkDeleteTasksAtomic(ids, force, store, context),
34758
35509
  start: (id, agentId) => startTask2(id, agentId, store),
34759
35510
  complete: (id, agentId, options2) => completeTask2(id, agentId, options2, store),
34760
35511
  fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, store),
@@ -34771,6 +35522,7 @@ function createPostgresTodosStorageAdapter(options) {
34771
35522
  add: (taskId, dependsOn, context) => addDependency2(taskId, dependsOn, store, context),
34772
35523
  remove: (taskId, dependsOn) => removeDependency2(taskId, dependsOn, store),
34773
35524
  list: (taskId) => listDependencies(taskId, store),
35525
+ listPage: (page) => store.listDependencyPage(page),
34774
35526
  listAll: () => store.list("dependencies")
34775
35527
  },
34776
35528
  verifications: {
@@ -34836,6 +35588,7 @@ function createPostgresTodosStorageAdapter(options) {
34836
35588
  get: (id) => store.get("agents", id),
34837
35589
  getByName: async (name) => matchAgentByName(await store.list("agents"), name),
34838
35590
  list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
35591
+ listPage: (options2) => store.listAgentPage(options2),
34839
35592
  update: (id, input) => updateAgent2(id, input, store),
34840
35593
  heartbeat: (idOrName, context) => heartbeatAgent(idOrName, store, context),
34841
35594
  release: (idOrName, sessionId, context) => releaseAgent2(idOrName, sessionId, store, context)
@@ -34893,8 +35646,9 @@ function createPostgresTodosStorageAdapter(options) {
34893
35646
  getCommentsPage: async (taskId, options2) => {
34894
35647
  return (await store.listComments(taskId, options2)).map(redactComment2).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
34895
35648
  },
34896
- getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
34897
- getRecentActivity: async (limit = 20) => (await store.list("audit_history")).sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, limit)
35649
+ getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => b.created_at.localeCompare(a.created_at) || b.id.localeCompare(a.id)),
35650
+ getTaskHistoryPage: (taskId, options2) => store.listTaskHistoryPage(taskId, options2),
35651
+ getRecentActivity: (limit = 20) => store.listRecentActivity(limit)
34898
35652
  },
34899
35653
  machines: createPostgresMachineRegistry(options.client, options.service ?? "todos", options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, () => store.ensureSchema()),
34900
35654
  atomicProjectMigration: createAtomicProjectMigration({ client: options.client, table: options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, service: options.service ?? "todos", ensureSchema: () => store.ensureSchema() }),
@@ -34931,6 +35685,8 @@ class PostgresJsonRecordStore {
34931
35685
  }
34932
35686
  async withDependencyGraphTransaction(fn) {
34933
35687
  await this.ensureSchema();
35688
+ if (this.projectIntegrityLocked)
35689
+ return fn(this);
34934
35690
  return this.withTaskParentIntegrityTransaction(async (client) => {
34935
35691
  const scoped = new PostgresJsonRecordStore({ ...this.options, client });
34936
35692
  scoped.schemaReady = Promise.resolve();
@@ -35077,6 +35833,128 @@ class PostgresJsonRecordStore {
35077
35833
  async list(type) {
35078
35834
  return (await this.listRecords(type)).map((record) => record.payload);
35079
35835
  }
35836
+ async listAgentPage(options) {
35837
+ const { limit, offset } = options;
35838
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
35839
+ throw new Error("Postgres agent page limit must be an integer from 1 to 500");
35840
+ }
35841
+ if (!Number.isSafeInteger(offset) || offset < 0) {
35842
+ throw new Error("Postgres agent page offset must be a non-negative integer");
35843
+ }
35844
+ await this.ensureSchema();
35845
+ const result = await this.options.client.query(`/* todos:list-agent-page */ WITH filtered AS (
35846
+ SELECT object_id, payload
35847
+ FROM ${this.tableName}
35848
+ WHERE service = $1 AND object_type = 'agents' AND deleted_at IS NULL
35849
+ AND ($2::boolean OR COALESCE(payload->>'status', 'active') <> 'archived')
35850
+ ), page AS (
35851
+ SELECT object_id, payload FROM filtered
35852
+ ORDER BY LOWER(payload->>'name'), object_id
35853
+ LIMIT $3 OFFSET $4
35854
+ )
35855
+ SELECT (SELECT count(*) FROM filtered) AS total,
35856
+ COALESCE((SELECT jsonb_agg(payload ORDER BY LOWER(payload->>'name'), object_id) FROM page), '[]'::jsonb) AS agents`, [this.service, options.include_archived === true, limit, offset]);
35857
+ const row = result.rows[0];
35858
+ const total = Number(row?.total);
35859
+ if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(row?.agents)) {
35860
+ throw new Error("Postgres agent page returned invalid count or agents");
35861
+ }
35862
+ if (row.agents.length > limit || row.agents.length > 0 && offset + row.agents.length > total) {
35863
+ throw new Error("Postgres agent page exceeded its requested bounds");
35864
+ }
35865
+ return { agents: row.agents.map((value) => payloadRecord2(value)), total };
35866
+ }
35867
+ async listRecentActivity(limit) {
35868
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4) {
35869
+ throw new Error("Postgres recent activity limit must be an integer from 1 to 10000");
35870
+ }
35871
+ await this.ensureSchema();
35872
+ const result = await this.options.client.query(`/* todos:list-recent-activity */ SELECT payload
35873
+ FROM ${this.tableName}
35874
+ WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
35875
+ ORDER BY payload->>'created_at' DESC, object_id DESC
35876
+ LIMIT $2`, [this.service, limit]);
35877
+ return result.rows.map((row) => payloadRecord2(row.payload));
35878
+ }
35879
+ async listTaskHistoryPage(taskId, options) {
35880
+ const { limit, offset } = options;
35881
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
35882
+ throw new Error("Postgres task history limit must be an integer from 1 to 500");
35883
+ }
35884
+ if (!Number.isSafeInteger(offset) || offset < 0) {
35885
+ throw new Error("Postgres task history offset must be a non-negative integer");
35886
+ }
35887
+ await this.ensureSchema();
35888
+ const direction = options.order === "asc" ? "ASC" : "DESC";
35889
+ const result = await this.options.client.query(`/* todos:list-task-history-page */ WITH filtered AS (
35890
+ SELECT object_id, payload, payload->>'created_at' AS created_at
35891
+ FROM ${this.tableName}
35892
+ WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
35893
+ AND payload->>'task_id' = $2
35894
+ AND ($3::text IS NULL OR payload->>'created_at' >= $3)
35895
+ AND ($4::text IS NULL OR payload->>'created_at' <= $4)
35896
+ ), page AS (
35897
+ SELECT object_id, payload, created_at FROM filtered
35898
+ ORDER BY created_at ${direction}, object_id ${direction}
35899
+ LIMIT $5 OFFSET $6
35900
+ )
35901
+ SELECT (SELECT count(*) FROM filtered) AS total,
35902
+ COALESCE((SELECT jsonb_agg(payload ORDER BY created_at ${direction}, object_id ${direction}) FROM page), '[]'::jsonb) AS history`, [this.service, taskId, options.since ?? null, options.until ?? null, limit, offset]);
35903
+ const row = result.rows[0];
35904
+ const total = Number(row?.total);
35905
+ if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(row?.history)) {
35906
+ throw new Error("Postgres task history page returned invalid count or history");
35907
+ }
35908
+ if (row.history.length > limit || row.history.length > 0 && offset + row.history.length > total) {
35909
+ throw new Error("Postgres task history page exceeded its requested bounds");
35910
+ }
35911
+ return { history: row.history.map((value) => payloadRecord2(value)), total };
35912
+ }
35913
+ async listDependencyPage(options) {
35914
+ const { limit, offset } = options;
35915
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
35916
+ throw new Error("Postgres dependency page limit must be an integer from 1 to 500");
35917
+ }
35918
+ if (!Number.isSafeInteger(offset) || offset < 0) {
35919
+ throw new Error("Postgres dependency page offset must be a non-negative integer");
35920
+ }
35921
+ await this.ensureSchema();
35922
+ const result = await this.options.client.query(`/* todos:list-dependencies-page */ WITH page AS (
35923
+ SELECT payload, updated_at, object_id
35924
+ FROM ${this.tableName}
35925
+ WHERE service = $1 AND object_type = 'dependencies' AND deleted_at IS NULL
35926
+ ORDER BY updated_at ASC, object_id ASC
35927
+ LIMIT $2 OFFSET $3
35928
+ )
35929
+ SELECT
35930
+ (SELECT COUNT(*)::text FROM ${this.tableName}
35931
+ WHERE service = $1 AND object_type = 'dependencies' AND deleted_at IS NULL) AS total,
35932
+ COALESCE(
35933
+ jsonb_agg(page.payload ORDER BY page.updated_at ASC, page.object_id ASC)
35934
+ FILTER (WHERE page.payload IS NOT NULL),
35935
+ '[]'::jsonb
35936
+ ) AS dependencies
35937
+ FROM page`, [this.service, limit, offset]);
35938
+ const row = result.rows[0];
35939
+ const totalText = typeof row?.total === "string" ? row.total : typeof row?.total === "number" && Number.isSafeInteger(row.total) ? String(row.total) : null;
35940
+ if (totalText === null || !/^\d+$/.test(totalText)) {
35941
+ throw new Error("Postgres dependency page returned an invalid total");
35942
+ }
35943
+ const total = Number(totalText);
35944
+ if (!Number.isSafeInteger(total)) {
35945
+ throw new Error("Postgres dependency page total exceeds the safe integer range");
35946
+ }
35947
+ if (!Array.isArray(row?.dependencies)) {
35948
+ throw new Error("Postgres dependency page returned an invalid dependencies array");
35949
+ }
35950
+ if (row.dependencies.length > limit || offset + row.dependencies.length > total) {
35951
+ throw new Error("Postgres dependency page returned rows outside the requested bound");
35952
+ }
35953
+ return {
35954
+ dependencies: row.dependencies.map((value) => payloadRecord2(value)),
35955
+ total
35956
+ };
35957
+ }
35080
35958
  async listComments(taskId, options = {}) {
35081
35959
  await this.ensureSchema();
35082
35960
  const limit = options.limit ?? 100;
@@ -35152,7 +36030,9 @@ class PostgresJsonRecordStore {
35152
36030
  return "1=0";
35153
36031
  return `${column} IN (${values.map((v) => p(v)).join(", ")})`;
35154
36032
  };
35155
- if (filter.include_archived === false)
36033
+ if (filter.archived_only === true)
36034
+ conds.push(`(payload->>'archived_at' IS NOT NULL)`);
36035
+ else if (filter.include_archived === false)
35156
36036
  conds.push(`(payload->>'archived_at' IS NULL)`);
35157
36037
  if (filter.ids)
35158
36038
  conds.push(inClause("payload->>'id'", filter.ids));
@@ -35209,7 +36089,7 @@ class PostgresJsonRecordStore {
35209
36089
  async listTasks(filter) {
35210
36090
  await this.ensureSchema();
35211
36091
  const { where, params, queryRef } = await this.buildTaskFilterSql(filter);
35212
- const orderBy = queryRef ? `ORDER BY ts_rank_cd(task_search_tsv, websearch_to_tsquery('simple', todos_immutable_unaccent(${queryRef}))) DESC, ${TASK_ORDER_TIEBREAK}` : TASK_ORDER_BY;
36092
+ const orderBy = filter.archived_only ? `ORDER BY payload->>'archived_at' DESC, object_id DESC` : queryRef ? `ORDER BY ts_rank_cd(task_search_tsv, websearch_to_tsquery('simple', todos_immutable_unaccent(${queryRef}))) DESC, ${TASK_ORDER_TIEBREAK}` : TASK_ORDER_BY;
35213
36093
  let sql = `/* todos:list-tasks */ SELECT payload FROM ${this.tableName} WHERE ${where} ${orderBy}`;
35214
36094
  if (filter.limit !== undefined) {
35215
36095
  params.push(filter.limit);
@@ -35429,6 +36309,8 @@ class PostgresJsonRecordStore {
35429
36309
  if (planIds.length === 0 && !parentGuard)
35430
36310
  return this.upsert("tasks", value, context);
35431
36311
  await this.ensureSchema();
36312
+ if (!queryClient && this.projectIntegrityLocked)
36313
+ queryClient = this.options.client;
35432
36314
  if (!queryClient) {
35433
36315
  return this.withTaskParentIntegrityTransaction((client2) => this.upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context, parentGuard, client2));
35434
36316
  }
@@ -35966,9 +36848,11 @@ class PostgresJsonRecordStore {
35966
36848
  }
35967
36849
  async deleteTaskHierarchy(id, context = {}) {
35968
36850
  await this.ensureSchema();
35969
- return this.withTaskParentIntegrityTransaction(async (client) => {
35970
- const timestamp2 = new Date().toISOString();
35971
- const result = await client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
36851
+ if (!this.projectIntegrityLocked) {
36852
+ return this.withDependencyGraphTransaction((scoped) => scoped.deleteTaskHierarchy(id, context));
36853
+ }
36854
+ const timestamp2 = new Date().toISOString();
36855
+ const result = await this.options.client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
35972
36856
  task_tree(object_id, path, cycle) AS (
35973
36857
  SELECT task.object_id, ARRAY[task.object_id], false
35974
36858
  FROM ${this.tableName} AS task
@@ -36030,13 +36914,12 @@ class PostgresJsonRecordStore {
36030
36914
  SELECT EXISTS (SELECT 1 FROM task_tree WHERE object_id = $2) AS found,
36031
36915
  (SELECT count(*) FROM tombstoned) AS deleted_count,
36032
36916
  (SELECT count(*) FROM tombstoned_related) AS related_deleted_count`, [
36033
- this.service,
36034
- id,
36035
- timestamp2,
36036
- context.requestId ?? this.sourceMachineId ?? null
36037
- ]);
36038
- return Boolean(result.rows[0]?.found);
36039
- });
36917
+ this.service,
36918
+ id,
36919
+ timestamp2,
36920
+ context.requestId ?? this.sourceMachineId ?? null
36921
+ ]);
36922
+ return Boolean(result.rows[0]?.found);
36040
36923
  }
36041
36924
  async getPlanProjectLinkReceipt(receiptId2) {
36042
36925
  const value = await this.get("plan_project_link_receipts", receiptId2);
@@ -36492,6 +37375,102 @@ class PostgresJsonRecordStore {
36492
37375
  updated_at = EXCLUDED.updated_at`, [this.service, name, value]);
36493
37376
  }
36494
37377
  }
37378
+ async function bulkCreateTasksAtomic(inputs, store, context) {
37379
+ return store.withDependencyGraphTransaction(async (scoped) => {
37380
+ const tempIds = new Map;
37381
+ const created = [];
37382
+ const dependencies = [];
37383
+ for (const input of inputs) {
37384
+ const { temp_id, depends_on: _dependsOn, ...taskInput } = input;
37385
+ const task = await createTask2(taskInput, scoped, context);
37386
+ if (temp_id)
37387
+ tempIds.set(temp_id, task.id);
37388
+ created.push({ temp_id: temp_id ?? null, id: task.id, short_id: task.short_id, title: task.title });
37389
+ }
37390
+ for (let index = 0;index < inputs.length; index++) {
37391
+ const input = inputs[index];
37392
+ const taskId = created[index].id;
37393
+ const seenDependencies = new Set;
37394
+ for (const reference of input.depends_on ?? []) {
37395
+ const exact = tempIds.get(reference) ?? (await scoped.get("tasks", reference))?.id;
37396
+ const dependencyId = exact ?? (await scoped.resolveTaskRef(reference))?.id;
37397
+ if (!dependencyId)
37398
+ throw new TaskNotFoundError(reference);
37399
+ if (seenDependencies.has(dependencyId)) {
37400
+ throw new ResourceConflictError("BULK_CREATE_DUPLICATE_DEPENDENCY", `Multiple references resolve to dependency ${dependencyId}`);
37401
+ }
37402
+ seenDependencies.add(dependencyId);
37403
+ dependencies.push(await addDependency2(taskId, dependencyId, scoped, context));
37404
+ }
37405
+ }
37406
+ return { schema_version: 1, atomic: true, created, dependencies };
37407
+ });
37408
+ }
37409
+ async function bulkDeleteTasksAtomic(ids, force, store, context) {
37410
+ return store.withDependencyGraphTransaction(async (scoped) => {
37411
+ const resolved = [];
37412
+ for (const reference of ids) {
37413
+ resolved.push({
37414
+ reference,
37415
+ task: await scoped.get("tasks", reference) ?? await scoped.resolveTaskRef(reference)
37416
+ });
37417
+ }
37418
+ const seen = new Set;
37419
+ for (const item of resolved) {
37420
+ if (!item.task)
37421
+ continue;
37422
+ if (seen.has(item.task.id)) {
37423
+ throw new ResourceConflictError("BULK_DELETE_DUPLICATE_TASK", `Multiple references resolve to task ${item.task.id}`);
37424
+ }
37425
+ seen.add(item.task.id);
37426
+ }
37427
+ const childState = new Map;
37428
+ for (const item of resolved) {
37429
+ if (!item.task)
37430
+ continue;
37431
+ const children = await scoped.listTasks({ parent_id: item.task.id, include_subtasks: true, limit: 1 });
37432
+ childState.set(item.task.id, children.length > 0);
37433
+ }
37434
+ const planned = resolved.filter((item) => item.task && (force || !childState.get(item.task.id)));
37435
+ const plannedIds = new Set(planned.map((item) => item.task.id));
37436
+ const roots = [];
37437
+ for (const item of planned) {
37438
+ if (!force) {
37439
+ roots.push(item);
37440
+ continue;
37441
+ }
37442
+ let parentId = item.task.parent_id;
37443
+ let covered = false;
37444
+ while (parentId) {
37445
+ if (plannedIds.has(parentId)) {
37446
+ covered = true;
37447
+ break;
37448
+ }
37449
+ parentId = (await scoped.get("tasks", parentId))?.parent_id ?? null;
37450
+ }
37451
+ if (!covered)
37452
+ roots.push(item);
37453
+ }
37454
+ for (const item of roots) {
37455
+ if (!await scoped.deleteTaskHierarchy(item.task.id, context)) {
37456
+ throw new Error(`Atomic bulk delete lost task ${item.task.id}`);
37457
+ }
37458
+ }
37459
+ return {
37460
+ schema_version: 1,
37461
+ atomic: true,
37462
+ force,
37463
+ results: resolved.map((item) => {
37464
+ if (!item.task)
37465
+ return { requested_id: item.reference, task_id: null, outcome: "missing", reason: "not_found" };
37466
+ if (!force && childState.get(item.task.id)) {
37467
+ return { requested_id: item.reference, task_id: item.task.id, outcome: "skipped", reason: "has_children" };
37468
+ }
37469
+ return { requested_id: item.reference, task_id: item.task.id, outcome: "deleted", reason: null };
37470
+ })
37471
+ };
37472
+ });
37473
+ }
36495
37474
  async function createTask2(input, store, context) {
36496
37475
  const timestamp2 = new Date().toISOString();
36497
37476
  const taskId = randomUUID6();
@@ -36582,12 +37561,16 @@ async function updateTask2(id, input, store, context) {
36582
37561
  }
36583
37562
  }
36584
37563
  }
37564
+ const patchTouchesPlacement = input.project_id !== undefined || input.task_list_id !== undefined;
37565
+ const effectiveProjectIdForShortId = input.project_id !== undefined ? input.project_id : existing.project_id;
37566
+ const backfilledShortId = existing.short_id === null && patchTouchesPlacement && effectiveProjectIdForShortId ? await nextTaskShortId2(effectiveProjectIdForShortId, store, context) : null;
36585
37567
  const reopened = existing.status === "completed" && input.status !== undefined && input.status !== "completed" && input.completed_at === undefined;
36586
37568
  const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
36587
37569
  const task = {
36588
37570
  ...existing,
36589
37571
  ...definedPatch(input),
36590
37572
  ...terminalNow ? { locked_by: null, locked_at: null } : {},
37573
+ short_id: backfilledShortId ?? existing.short_id,
36591
37574
  version: existing.version + 1,
36592
37575
  updated_at: new Date().toISOString(),
36593
37576
  tags: input.tags ?? existing.tags,
@@ -44847,7 +45830,7 @@ var init_token_utils = __esm(() => {
44847
45830
  "machines_topology",
44848
45831
  "machines_unarchive"
44849
45832
  ],
44850
- maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
45833
+ maintenance: ["get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task"]
44851
45834
  };
44852
45835
  MCP_PROFILE_GROUPS = {
44853
45836
  minimal: ["core", "loops"],
@@ -56620,6 +57603,19 @@ var init_client_ip = __esm(() => {
56620
57603
  TRUST_TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
56621
57604
  });
56622
57605
 
57606
+ // src/server/rate-limit-config.ts
57607
+ function resolveRateLimitMax(raw) {
57608
+ const value = raw?.trim();
57609
+ if (!value)
57610
+ return DEFAULT_RATE_LIMIT_MAX;
57611
+ const maximum = Number(value);
57612
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(maximum) || maximum < 1 || maximum > MAX_RATE_LIMIT_MAX) {
57613
+ throw new Error(`HASNA_TODOS_RATE_LIMIT_MAX (legacy TODOS_RATE_LIMIT_MAX) must be an integer from 1 to ${MAX_RATE_LIMIT_MAX}.`);
57614
+ }
57615
+ return maximum;
57616
+ }
57617
+ var DEFAULT_RATE_LIMIT_MAX = 12000, MAX_RATE_LIMIT_MAX = 1e6;
57618
+
56623
57619
  // src/storage/shadow-outbox.ts
56624
57620
  class TodosShadowOutbox {
56625
57621
  db;
@@ -57008,6 +58004,11 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
57008
58004
  archived_at: { type: ["string", "null"] }
57009
58005
  } },
57010
58006
  Task: taskSchema,
58007
+ TaskDependency: taskDependencySchema,
58008
+ DependencyPage: dependencyPageSchema,
58009
+ BulkCreateTaskInput: bulkCreateTaskInputSchema,
58010
+ BulkCreateReceipt: bulkCreateReceiptSchema,
58011
+ BulkDeleteReceipt: bulkDeleteReceiptSchema,
57011
58012
  Project: projectSchema,
57012
58013
  TaskManifestBounds: taskManifestBoundsSchema,
57013
58014
  TaskManifestCapability: taskManifestCapabilitySchema,
@@ -57088,6 +58089,7 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
57088
58089
  parent_id: { type: "string", nullable: true },
57089
58090
  plan_id: { type: "string", nullable: true },
57090
58091
  task_list_id: { type: "string", nullable: true },
58092
+ archived_at: { type: "string", format: "date-time", nullable: true },
57091
58093
  version: { type: "number" }
57092
58094
  }
57093
58095
  },
@@ -58732,6 +59734,7 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
58732
59734
  { name: "parent_id", in: "query", schema: { type: "string", nullable: true } },
58733
59735
  { name: "include_subtasks", in: "query", schema: { type: "boolean" } },
58734
59736
  { name: "include_archived", in: "query", schema: { type: "boolean" }, description: "Explicit archive selection; false excludes archived rows" },
59737
+ { name: "archived_only", in: "query", schema: { type: "boolean" }, description: "Select only rows carrying archived_at; applied before bounded LIMIT/OFFSET and implies include_archived" },
58735
59738
  { name: "plan_read_contract", in: "query", schema: { type: "string", enum: ["1"] }, description: "Requires plan_id and explicit include_subtasks=true/include_archived; returns a versioned selection receipt" },
58736
59739
  { name: "plan_id", in: "query", schema: { type: "string" } },
58737
59740
  { name: "task_list_id", in: "query", schema: { type: "string" } },
@@ -58784,6 +59787,66 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
58784
59787
  }
58785
59788
  }
58786
59789
  },
59790
+ "/v1/tasks/bulk-create": {
59791
+ post: {
59792
+ operationId: "bulkCreateTasks",
59793
+ summary: "Atomically create a bounded task batch and its dependency edges",
59794
+ requestBody: {
59795
+ required: true,
59796
+ content: {
59797
+ "application/json": {
59798
+ schema: {
59799
+ type: "object",
59800
+ additionalProperties: false,
59801
+ required: ["schema_version", "tasks"],
59802
+ properties: {
59803
+ schema_version: { type: "integer", enum: [1] },
59804
+ tasks: { type: "array", minItems: 1, maxItems: 50, items: { $ref: "#/components/schemas/BulkCreateTaskInput" } }
59805
+ }
59806
+ }
59807
+ }
59808
+ }
59809
+ },
59810
+ responses: {
59811
+ "201": { content: { "application/json": { schema: { type: "object", required: ["receipt"], properties: { receipt: { $ref: "#/components/schemas/BulkCreateReceipt" } } } } } },
59812
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
59813
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
59814
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
59815
+ "413": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
59816
+ "501": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
59817
+ }
59818
+ }
59819
+ },
59820
+ "/v1/tasks/bulk-delete": {
59821
+ post: {
59822
+ operationId: "bulkDeleteTasks",
59823
+ summary: "Atomically delete a bounded task set using authoritative child checks",
59824
+ requestBody: {
59825
+ required: true,
59826
+ content: {
59827
+ "application/json": {
59828
+ schema: {
59829
+ type: "object",
59830
+ additionalProperties: false,
59831
+ required: ["schema_version", "task_ids"],
59832
+ properties: {
59833
+ schema_version: { type: "integer", enum: [1] },
59834
+ task_ids: { type: "array", minItems: 1, maxItems: 100, uniqueItems: true, items: { type: "string", minLength: 1, maxLength: 200 } },
59835
+ force: { type: "boolean", default: false, description: "Requires todos:* scope when true" }
59836
+ }
59837
+ }
59838
+ }
59839
+ }
59840
+ },
59841
+ responses: {
59842
+ "200": { content: { "application/json": { schema: { type: "object", required: ["receipt"], properties: { receipt: { $ref: "#/components/schemas/BulkDeleteReceipt" } } } } } },
59843
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
59844
+ "403": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
59845
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
59846
+ "501": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
59847
+ }
59848
+ }
59849
+ },
58787
59850
  "/v1/tasks/{id}": {
58788
59851
  get: {
58789
59852
  operationId: "getTask",
@@ -58832,6 +59895,42 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
58832
59895
  }
58833
59896
  }
58834
59897
  },
59898
+ "/v1/tasks/{id}/history": {
59899
+ get: {
59900
+ operationId: "listTaskHistory",
59901
+ summary: "List a storage-bounded page of one task's audit history",
59902
+ description: "The storage adapter applies task scope, time filters, deterministic order, LIMIT, and OFFSET before materializing rows. Omission uses a bounded 500-row compatibility window and returns 426 when incomplete.",
59903
+ parameters: [
59904
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
59905
+ { name: "limit", in: "query", required: false, schema: { type: "integer", minimum: 1, maximum: 500, default: 500 } },
59906
+ { name: "offset", in: "query", required: false, schema: { type: "integer", minimum: 0, default: 0 } },
59907
+ { name: "since", in: "query", schema: { type: "string", format: "date-time" } },
59908
+ { name: "until", in: "query", schema: { type: "string", format: "date-time" } },
59909
+ { name: "order", in: "query", schema: { type: "string", enum: ["asc", "desc"], default: "desc" } }
59910
+ ],
59911
+ responses: {
59912
+ "200": { content: { "application/json": { schema: {
59913
+ type: "object",
59914
+ additionalProperties: false,
59915
+ required: ["history", "count", "total", "limit", "offset", "order", "has_more", "next_offset"],
59916
+ properties: {
59917
+ history: { type: "array", maxItems: 500, items: { type: "object", additionalProperties: true } },
59918
+ count: { type: "integer", minimum: 0, maximum: 500 },
59919
+ total: { type: "integer", minimum: 0 },
59920
+ limit: { type: "integer", minimum: 1, maximum: 500 },
59921
+ offset: { type: "integer", minimum: 0 },
59922
+ order: { type: "string", enum: ["asc", "desc"] },
59923
+ has_more: { type: "boolean" },
59924
+ next_offset: { type: "integer", minimum: 0, nullable: true }
59925
+ }
59926
+ } } } },
59927
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
59928
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
59929
+ "426": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
59930
+ "501": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
59931
+ }
59932
+ }
59933
+ },
58835
59934
  "/v1/tasks/{id}/comments": {
58836
59935
  get: {
58837
59936
  operationId: "listTaskComments",
@@ -59890,6 +60989,95 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
59890
60989
  }
59891
60990
  }
59892
60991
  },
60992
+ "/v1/agents": {
60993
+ get: {
60994
+ operationId: "listAgents",
60995
+ summary: "List the shared agent roster with storage-bounded pagination",
60996
+ description: "The authority applies a maximum 500-row LIMIT/OFFSET before materializing agents. Omitting pagination uses a 500-row legacy window and returns 426 when the roster is larger.",
60997
+ parameters: [
60998
+ { name: "limit", in: "query", required: false, schema: { type: "integer", minimum: 1, maximum: 500, default: 500 } },
60999
+ { name: "offset", in: "query", required: false, schema: { type: "integer", minimum: 0, default: 0 } },
61000
+ { name: "include_archived", in: "query", required: false, schema: { type: "boolean", default: false } }
61001
+ ],
61002
+ responses: {
61003
+ "200": {
61004
+ content: {
61005
+ "application/json": {
61006
+ schema: {
61007
+ type: "object",
61008
+ required: ["agents", "count", "total", "limit", "offset", "has_more", "next_offset"],
61009
+ properties: {
61010
+ agents: { type: "array", items: { type: "object" } },
61011
+ count: { type: "integer", minimum: 0 },
61012
+ total: { type: "integer", minimum: 0 },
61013
+ limit: { type: "integer", minimum: 1, maximum: 500 },
61014
+ offset: { type: "integer", minimum: 0 },
61015
+ has_more: { type: "boolean" },
61016
+ next_offset: { anyOf: [{ type: "integer", minimum: 0 }, { type: "null" }] }
61017
+ }
61018
+ }
61019
+ }
61020
+ }
61021
+ },
61022
+ "426": { description: "Legacy unpaged request exceeded the bounded roster window" }
61023
+ }
61024
+ }
61025
+ },
61026
+ "/v1/activity": {
61027
+ get: {
61028
+ operationId: "listRecentActivity",
61029
+ summary: "List a bounded recent task-history window",
61030
+ parameters: [
61031
+ { name: "limit", in: "query", required: false, schema: { type: "integer", minimum: 1, maximum: 1e4, default: 50 } }
61032
+ ],
61033
+ responses: {
61034
+ "200": {
61035
+ content: {
61036
+ "application/json": {
61037
+ schema: {
61038
+ type: "object",
61039
+ required: ["activity", "count", "limit"],
61040
+ properties: {
61041
+ activity: { type: "array", items: { type: "object" } },
61042
+ count: { type: "integer", minimum: 0 },
61043
+ limit: { type: "integer", minimum: 1, maximum: 1e4 }
61044
+ }
61045
+ }
61046
+ }
61047
+ }
61048
+ }
61049
+ }
61050
+ }
61051
+ },
61052
+ "/v1/dependencies": {
61053
+ get: {
61054
+ operationId: "listDependencies",
61055
+ summary: "List dependency edges with storage-bounded pagination",
61056
+ description: "The authority always applies a maximum 500-row LIMIT/OFFSET before materializing edges. Omitting both parameters uses limit=500 and offset=0 only when that window is complete; a larger graph returns 426 so an old client cannot mistake a partial page for the whole graph.",
61057
+ parameters: [
61058
+ {
61059
+ name: "limit",
61060
+ in: "query",
61061
+ required: false,
61062
+ description: "Maximum edges to materialize. Omission defaults to the bounded 500-row legacy window.",
61063
+ schema: { type: "integer", minimum: 1, maximum: 500, default: 500 }
61064
+ },
61065
+ {
61066
+ name: "offset",
61067
+ in: "query",
61068
+ required: false,
61069
+ description: "Zero-based dependency-edge offset in stable storage order.",
61070
+ schema: { type: "integer", minimum: 0, default: 0 }
61071
+ }
61072
+ ],
61073
+ responses: {
61074
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/DependencyPage" } } } },
61075
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
61076
+ "426": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
61077
+ "501": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
61078
+ }
61079
+ }
61080
+ },
59893
61081
  "/v1/task-lists": {
59894
61082
  get: {
59895
61083
  operationId: "listTaskLists",
@@ -60137,7 +61325,7 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
60137
61325
  }
60138
61326
  });
60139
61327
  }
60140
- var atomicMigrationAuthoritySchema, atomicMigrationReceiptSchema, 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;
61328
+ var atomicMigrationAuthoritySchema, atomicMigrationReceiptSchema, taskSchema, taskDependencySchema, bulkCreateTaskInputSchema, bulkCreateReceiptSchema, bulkDeleteReceiptSchema, dependencyPageSchema, 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;
60141
61329
  var init_openapi = __esm(() => {
60142
61330
  init_package_version();
60143
61331
  init_types();
@@ -60165,6 +61353,103 @@ var init_openapi = __esm(() => {
60165
61353
  updated_at: { type: "string" }
60166
61354
  }
60167
61355
  };
61356
+ taskDependencySchema = {
61357
+ type: "object",
61358
+ additionalProperties: false,
61359
+ required: ["task_id", "depends_on"],
61360
+ properties: {
61361
+ task_id: { type: "string", minLength: 1 },
61362
+ depends_on: { type: "string", minLength: 1 },
61363
+ external_project_id: { type: "string", nullable: true },
61364
+ external_task_id: { type: "string", nullable: true }
61365
+ }
61366
+ };
61367
+ bulkCreateTaskInputSchema = {
61368
+ type: "object",
61369
+ additionalProperties: false,
61370
+ required: ["title"],
61371
+ properties: {
61372
+ temp_id: { type: "string", minLength: 1, maxLength: 100 },
61373
+ depends_on: { type: "array", maxItems: 100, items: { type: "string", minLength: 1, maxLength: 200 } },
61374
+ title: { type: "string", minLength: 1, maxLength: 20000 },
61375
+ description: { type: "string", nullable: true, maxLength: 1e5 },
61376
+ status: { type: "string", enum: [...TASK_STATUSES] },
61377
+ priority: { type: "string", enum: [...TASK_PRIORITIES] },
61378
+ project_id: { type: "string", minLength: 1, maxLength: 200 },
61379
+ parent_id: { type: "string" },
61380
+ plan_id: { type: "string" },
61381
+ task_list_id: { type: "string", minLength: 1, maxLength: 200 },
61382
+ assigned_to: { type: "string", minLength: 1, maxLength: 200 },
61383
+ agent_id: { type: "string" },
61384
+ created_by: { type: "string" },
61385
+ tags: { type: "array", maxItems: 100, items: { type: "string", minLength: 1, maxLength: 100 } },
61386
+ estimated_minutes: { type: "number", minimum: 0 }
61387
+ }
61388
+ };
61389
+ bulkCreateReceiptSchema = {
61390
+ type: "object",
61391
+ additionalProperties: false,
61392
+ required: ["schema_version", "atomic", "created", "dependencies"],
61393
+ properties: {
61394
+ schema_version: { type: "integer", enum: [1] },
61395
+ atomic: { type: "boolean", enum: [true] },
61396
+ created: {
61397
+ type: "array",
61398
+ maxItems: 100,
61399
+ items: {
61400
+ type: "object",
61401
+ additionalProperties: false,
61402
+ required: ["temp_id", "id", "short_id", "title"],
61403
+ properties: {
61404
+ temp_id: { type: "string", nullable: true },
61405
+ id: { type: "string" },
61406
+ short_id: { type: "string", nullable: true },
61407
+ title: { type: "string" }
61408
+ }
61409
+ }
61410
+ },
61411
+ dependencies: { type: "array", maxItems: 100, items: { $ref: "#/components/schemas/TaskDependency" } }
61412
+ }
61413
+ };
61414
+ bulkDeleteReceiptSchema = {
61415
+ type: "object",
61416
+ additionalProperties: false,
61417
+ required: ["schema_version", "atomic", "force", "results"],
61418
+ properties: {
61419
+ schema_version: { type: "integer", enum: [1] },
61420
+ atomic: { type: "boolean", enum: [true] },
61421
+ force: { type: "boolean" },
61422
+ results: {
61423
+ type: "array",
61424
+ maxItems: 100,
61425
+ items: {
61426
+ type: "object",
61427
+ additionalProperties: false,
61428
+ required: ["requested_id", "task_id", "outcome", "reason"],
61429
+ properties: {
61430
+ requested_id: { type: "string" },
61431
+ task_id: { type: "string", nullable: true },
61432
+ outcome: { type: "string", enum: ["deleted", "skipped", "missing"] },
61433
+ reason: { oneOf: [{ type: "string", enum: ["has_children", "not_found"] }, { type: "null" }] }
61434
+ }
61435
+ }
61436
+ }
61437
+ }
61438
+ };
61439
+ dependencyPageSchema = {
61440
+ type: "object",
61441
+ additionalProperties: false,
61442
+ required: ["dependencies", "count", "total", "limit", "offset", "has_more", "next_offset"],
61443
+ properties: {
61444
+ dependencies: { type: "array", items: { $ref: "#/components/schemas/TaskDependency" } },
61445
+ count: { type: "integer", minimum: 0 },
61446
+ total: { type: "integer", minimum: 0 },
61447
+ limit: { type: "integer", minimum: 0 },
61448
+ offset: { type: "integer", minimum: 0 },
61449
+ has_more: { type: "boolean" },
61450
+ next_offset: { type: "integer", minimum: 0, nullable: true }
61451
+ }
61452
+ };
60168
61453
  taskManifestBoundsSchema = {
60169
61454
  type: "object",
60170
61455
  additionalProperties: false,
@@ -61690,6 +62975,9 @@ function validateTaskPatchVocabulary(value) {
61690
62975
  if (!parsed.ok)
61691
62976
  return { ok: false, message: parsed.message };
61692
62977
  }
62978
+ if (body2.archived_at !== undefined && body2.archived_at !== null && (typeof body2.archived_at !== "string" || !body2.archived_at.trim())) {
62979
+ return { ok: false, message: "archived_at must be a non-empty timestamp string or null" };
62980
+ }
61693
62981
  if (body2.parent_id !== undefined && body2.parent_id !== null && (typeof body2.parent_id !== "string" || !body2.parent_id.trim())) {
61694
62982
  return { ok: false, message: "parent_id must be a non-empty task id or null" };
61695
62983
  }
@@ -61939,6 +63227,147 @@ function validateTemplatePatch(value) {
61939
63227
  ...body2.plan_id === null ? { plan_id: null } : {}
61940
63228
  } };
61941
63229
  }
63230
+ function validateBulkCreateRequest(value) {
63231
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
63232
+ return { ok: false, message: "bulk create body must be an object" };
63233
+ }
63234
+ const body2 = value;
63235
+ const allowedTop = new Set(["schema_version", "tasks"]);
63236
+ const unknownTop = Object.keys(body2).find((key3) => !allowedTop.has(key3));
63237
+ if (unknownTop)
63238
+ return { ok: false, message: `unknown bulk create field: ${unknownTop}` };
63239
+ if (body2.schema_version !== 1)
63240
+ return { ok: false, message: "schema_version must be 1" };
63241
+ if (!Array.isArray(body2.tasks) || body2.tasks.length < 1 || body2.tasks.length > MAX_BULK_CREATE_TASKS) {
63242
+ return { ok: false, message: `tasks must contain between 1 and ${MAX_BULK_CREATE_TASKS} items` };
63243
+ }
63244
+ const allowedTask = new Set([
63245
+ "temp_id",
63246
+ "depends_on",
63247
+ "title",
63248
+ "description",
63249
+ "status",
63250
+ "priority",
63251
+ "project_id",
63252
+ "parent_id",
63253
+ "plan_id",
63254
+ "task_list_id",
63255
+ "agent_id",
63256
+ "created_by",
63257
+ "assigned_to",
63258
+ "tags",
63259
+ "estimated_minutes"
63260
+ ]);
63261
+ const tempIds = new Set;
63262
+ const tasks = [];
63263
+ let dependencyCount = 0;
63264
+ for (let index = 0;index < body2.tasks.length; index++) {
63265
+ const raw = body2.tasks[index];
63266
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
63267
+ return { ok: false, message: `tasks[${index}] must be an object` };
63268
+ }
63269
+ const task2 = raw;
63270
+ const unknown = Object.keys(task2).find((key3) => !allowedTask.has(key3));
63271
+ if (unknown)
63272
+ return { ok: false, message: `unknown tasks[${index}] field: ${unknown}` };
63273
+ if (typeof task2.title !== "string" || !task2.title.trim() || task2.title.length > 20000) {
63274
+ return { ok: false, message: `tasks[${index}].title must be a non-empty string of at most 20000 characters` };
63275
+ }
63276
+ if (task2.description !== undefined && task2.description !== null && (typeof task2.description !== "string" || task2.description.length > 1e5)) {
63277
+ return { ok: false, message: `tasks[${index}].description must be a string or null of at most 100000 characters` };
63278
+ }
63279
+ if (task2.status !== undefined && (typeof task2.status !== "string" || !TASK_STATUSES.includes(task2.status))) {
63280
+ return { ok: false, message: `tasks[${index}].status is invalid` };
63281
+ }
63282
+ if (task2.priority !== undefined && (typeof task2.priority !== "string" || !TASK_PRIORITIES.includes(task2.priority))) {
63283
+ return { ok: false, message: `tasks[${index}].priority is invalid` };
63284
+ }
63285
+ for (const field of ["project_id", "parent_id", "plan_id", "task_list_id", "agent_id", "created_by", "assigned_to"]) {
63286
+ const candidate = task2[field];
63287
+ if (candidate !== undefined && (typeof candidate !== "string" || !candidate.trim() || candidate.length > 200)) {
63288
+ return { ok: false, message: `tasks[${index}].${field} must be a non-empty string of at most 200 characters` };
63289
+ }
63290
+ }
63291
+ if (task2.tags !== undefined && (!Array.isArray(task2.tags) || task2.tags.length > 100 || task2.tags.some((tag) => typeof tag !== "string" || !tag.trim() || tag.length > 100))) {
63292
+ return { ok: false, message: `tasks[${index}].tags must be an array of at most 100 non-empty strings` };
63293
+ }
63294
+ if (task2.estimated_minutes !== undefined && (typeof task2.estimated_minutes !== "number" || !Number.isFinite(task2.estimated_minutes) || task2.estimated_minutes < 0)) {
63295
+ return { ok: false, message: `tasks[${index}].estimated_minutes must be a finite non-negative number` };
63296
+ }
63297
+ if (task2.temp_id !== undefined) {
63298
+ if (typeof task2.temp_id !== "string" || !task2.temp_id.trim() || task2.temp_id.length > 100) {
63299
+ return { ok: false, message: `tasks[${index}].temp_id must be a non-empty string of at most 100 characters` };
63300
+ }
63301
+ if (tempIds.has(task2.temp_id))
63302
+ return { ok: false, message: `duplicate temp_id: ${task2.temp_id}` };
63303
+ tempIds.add(task2.temp_id);
63304
+ }
63305
+ if (task2.depends_on !== undefined && (!Array.isArray(task2.depends_on) || task2.depends_on.length > MAX_BULK_DEPENDENCIES || task2.depends_on.some((ref) => typeof ref !== "string" || !ref.trim() || ref.length > 200))) {
63306
+ return { ok: false, message: `tasks[${index}].depends_on must be an array of at most ${MAX_BULK_DEPENDENCIES} non-empty task references` };
63307
+ }
63308
+ if (Array.isArray(task2.depends_on) && new Set(task2.depends_on).size !== task2.depends_on.length) {
63309
+ return { ok: false, message: `tasks[${index}].depends_on must not contain duplicates` };
63310
+ }
63311
+ dependencyCount += Array.isArray(task2.depends_on) ? task2.depends_on.length : 0;
63312
+ if (dependencyCount > MAX_BULK_DEPENDENCIES) {
63313
+ return { ok: false, message: `bulk create supports at most ${MAX_BULK_DEPENDENCIES} dependency edges` };
63314
+ }
63315
+ tasks.push(task2);
63316
+ }
63317
+ return { ok: true, tasks };
63318
+ }
63319
+ function validateBulkDeleteRequest(value) {
63320
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
63321
+ return { ok: false, message: "bulk delete body must be an object" };
63322
+ }
63323
+ const body2 = value;
63324
+ const allowed = new Set(["schema_version", "task_ids", "force"]);
63325
+ const unknown = Object.keys(body2).find((key3) => !allowed.has(key3));
63326
+ if (unknown)
63327
+ return { ok: false, message: `unknown bulk delete field: ${unknown}` };
63328
+ if (body2.schema_version !== 1)
63329
+ return { ok: false, message: "schema_version must be 1" };
63330
+ if (!Array.isArray(body2.task_ids) || body2.task_ids.length < 1 || body2.task_ids.length > MAX_BULK_DELETE_TASKS) {
63331
+ return { ok: false, message: `task_ids must contain between 1 and ${MAX_BULK_DELETE_TASKS} items` };
63332
+ }
63333
+ if (body2.task_ids.some((id) => typeof id !== "string" || !id.trim() || id.length > 200)) {
63334
+ return { ok: false, message: "task_ids must contain non-empty task references of at most 200 characters" };
63335
+ }
63336
+ if (new Set(body2.task_ids).size !== body2.task_ids.length) {
63337
+ return { ok: false, message: "task_ids must not contain duplicates" };
63338
+ }
63339
+ if (body2.force !== undefined && typeof body2.force !== "boolean") {
63340
+ return { ok: false, message: "force must be a boolean" };
63341
+ }
63342
+ return { ok: true, taskIds: body2.task_ids, force: body2.force === true };
63343
+ }
63344
+ async function readBoundedJson(req, maxBytes) {
63345
+ const reader = req.body?.getReader();
63346
+ if (!reader)
63347
+ return { ok: false, status: 400, message: "missing JSON body" };
63348
+ let size = 0;
63349
+ const chunks = [];
63350
+ try {
63351
+ while (true) {
63352
+ const part = await reader.read();
63353
+ if (part.done)
63354
+ break;
63355
+ size += part.value.byteLength;
63356
+ if (size > maxBytes) {
63357
+ await reader.cancel();
63358
+ return { ok: false, status: 413, message: `request exceeds the ${maxBytes}-byte limit` };
63359
+ }
63360
+ chunks.push(part.value);
63361
+ }
63362
+ } finally {
63363
+ reader.releaseLock();
63364
+ }
63365
+ try {
63366
+ return { ok: true, value: JSON.parse(Buffer.concat(chunks).toString("utf8")) };
63367
+ } catch {
63368
+ return { ok: false, status: 400, message: "invalid JSON body" };
63369
+ }
63370
+ }
61942
63371
  async function readJson3(req) {
61943
63372
  try {
61944
63373
  const text = await req.text();
@@ -62174,6 +63603,50 @@ async function handleV1Request(req, url, dependencies = {}) {
62174
63603
  return error(405, "Use GET or POST /v1/machines");
62175
63604
  }
62176
63605
  if (resource === "tasks") {
63606
+ if (id === "bulk-create" && !action) {
63607
+ if (method !== "POST")
63608
+ return error(405, "bulk task creation requires POST");
63609
+ if (typeof store.tasks.bulkCreateAtomic !== "function") {
63610
+ return error(501, "Upgrade the Todos backend: atomic bulk task creation is unavailable");
63611
+ }
63612
+ const body2 = await readBoundedJson(req, 2 * 1024 * 1024);
63613
+ if (!body2.ok)
63614
+ return error(body2.status, body2.message);
63615
+ const parsed = validateBulkCreateRequest(body2.value);
63616
+ if (!parsed.ok)
63617
+ return error(400, parsed.message);
63618
+ const normalizedTasks = [];
63619
+ for (const task2 of parsed.tasks) {
63620
+ if (!task2.assigned_to) {
63621
+ normalizedTasks.push(task2);
63622
+ continue;
63623
+ }
63624
+ const assigned = await store.agents.get(task2.assigned_to) ?? await store.agents.getByName(task2.assigned_to);
63625
+ if (!assigned)
63626
+ return error(404, `assigned agent not found: ${task2.assigned_to}`);
63627
+ normalizedTasks.push({ ...task2, assigned_to: assigned.id });
63628
+ }
63629
+ const receipt = await store.tasks.bulkCreateAtomic(normalizedTasks, contextFromPrincipal(principal));
63630
+ return json6({ receipt }, 201);
63631
+ }
63632
+ if (id === "bulk-delete" && !action) {
63633
+ if (method !== "POST")
63634
+ return error(405, "bulk task deletion requires POST");
63635
+ if (typeof store.tasks.bulkDeleteAtomic !== "function") {
63636
+ return error(501, "Upgrade the Todos backend: atomic bulk task deletion is unavailable");
63637
+ }
63638
+ const body2 = await readBoundedJson(req, 64 * 1024);
63639
+ if (!body2.ok)
63640
+ return error(body2.status, body2.message);
63641
+ const parsed = validateBulkDeleteRequest(body2.value);
63642
+ if (!parsed.ok)
63643
+ return error(400, parsed.message);
63644
+ if (parsed.force && !principal.scopes.includes("todos:*")) {
63645
+ return error(403, "force bulk deletion requires todos:* scope");
63646
+ }
63647
+ const receipt = await store.tasks.bulkDeleteAtomic(parsed.taskIds, parsed.force, contextFromPrincipal(principal));
63648
+ return json6({ receipt });
63649
+ }
62177
63650
  if (id === "exists" && !action) {
62178
63651
  if (method !== "POST")
62179
63652
  return error(405, `method ${method} not allowed on /v1/tasks/exists`);
@@ -62259,6 +63732,13 @@ async function handleV1Request(req, url, dependencies = {}) {
62259
63732
  if (includeArchived !== null && includeArchived !== "true" && includeArchived !== "false") {
62260
63733
  return error(400, "include_archived must be true or false");
62261
63734
  }
63735
+ const archivedOnly = url.searchParams.get("archived_only");
63736
+ if (archivedOnly !== null && archivedOnly !== "true" && archivedOnly !== "false") {
63737
+ return error(400, "archived_only must be true or false");
63738
+ }
63739
+ if (archivedOnly === "true" && includeArchived === "false") {
63740
+ return error(400, "archived_only=true is incompatible with include_archived=false");
63741
+ }
62262
63742
  const planRead = url.searchParams.get("plan_read_contract");
62263
63743
  if (planRead !== null) {
62264
63744
  const keys = [...url.searchParams.keys()];
@@ -62297,6 +63777,7 @@ async function handleV1Request(req, url, dependencies = {}) {
62297
63777
  return offsetParam.response;
62298
63778
  const filter = {
62299
63779
  ...includeArchived !== null ? { include_archived: includeArchived === "true" } : {},
63780
+ ...archivedOnly === "true" ? { archived_only: true, include_archived: true } : {},
62300
63781
  ...updatedAfter !== null && updatedAfter.ok ? { updated_after: updatedAfter.value } : {},
62301
63782
  ...url.searchParams.get("q") ? { query: url.searchParams.get("q") } : {},
62302
63783
  ...statusParam.value !== undefined ? { status: statusParam.value } : {},
@@ -62468,8 +63949,48 @@ async function handleV1Request(req, url, dependencies = {}) {
62468
63949
  return error(405, `method ${method} not allowed on /v1/tasks/:id/history`);
62469
63950
  if (!await store.tasks.get(id))
62470
63951
  return error(404, "task not found");
62471
- const history = await store.audit.getTaskHistory(id);
62472
- return json6({ history, count: history.length });
63952
+ if (typeof store.audit.getTaskHistoryPage !== "function") {
63953
+ return error(501, "bounded task history is not supported by this storage backend");
63954
+ }
63955
+ const rawLimit = url.searchParams.get("limit");
63956
+ const rawOffset = url.searchParams.get("offset");
63957
+ const requestedPage = rawLimit !== null || rawOffset !== null || url.searchParams.has("order") || url.searchParams.has("since") || url.searchParams.has("until");
63958
+ const limitParam = paginationQueryParam(url, "limit");
63959
+ if (!limitParam.ok)
63960
+ return limitParam.response;
63961
+ const offsetParam = paginationQueryParam(url, "offset");
63962
+ if (!offsetParam.ok)
63963
+ return offsetParam.response;
63964
+ const limit = limitParam.value ?? 500;
63965
+ const offset = offsetParam.value ?? 0;
63966
+ if (limit > 500)
63967
+ return error(400, "history limit must be at most 500");
63968
+ const order = url.searchParams.get("order") ?? "desc";
63969
+ if (order !== "asc" && order !== "desc")
63970
+ return error(400, "history order must be asc or desc");
63971
+ const since = url.searchParams.get("since") ?? undefined;
63972
+ const until = url.searchParams.get("until") ?? undefined;
63973
+ const result = await store.audit.getTaskHistoryPage(id, { limit, offset, order, ...since ? { since } : {}, ...until ? { until } : {} }, contextFromPrincipal(principal));
63974
+ if (!result || !Array.isArray(result.history) || !Number.isSafeInteger(result.total) || result.total < 0 || result.history.length > limit || result.history.length > 0 && offset + result.history.length > result.total) {
63975
+ return error(500, "TASK_HISTORY_PAGE_INVALID: storage returned contradictory pagination");
63976
+ }
63977
+ const hasMore = offset + result.history.length < result.total;
63978
+ if (!requestedPage && hasMore) {
63979
+ return error(426, "task history exceeds the bounded legacy window; send limit, offset, and order with an upgraded client");
63980
+ }
63981
+ if (hasMore && result.history.length === 0) {
63982
+ return error(500, "TASK_HISTORY_PAGE_STALLED: storage returned no progress before total");
63983
+ }
63984
+ return json6({
63985
+ history: result.history,
63986
+ count: result.history.length,
63987
+ total: result.total,
63988
+ limit,
63989
+ offset,
63990
+ order,
63991
+ has_more: hasMore,
63992
+ next_offset: hasMore ? offset + result.history.length : null
63993
+ });
62473
63994
  }
62474
63995
  if (action === "lock" || action === "unlock") {
62475
63996
  if (method !== "POST")
@@ -63111,8 +64632,48 @@ async function handleV1Request(req, url, dependencies = {}) {
63111
64632
  }
63112
64633
  if (resource === "agents") {
63113
64634
  if (!id && method === "GET") {
63114
- const agents = await store.agents.list();
63115
- return json6({ agents, count: agents.length });
64635
+ if (typeof store.agents.listPage !== "function") {
64636
+ return error(501, "bounded agent pagination is not supported by this storage backend");
64637
+ }
64638
+ const rawLimit = url.searchParams.get("limit");
64639
+ const rawOffset = url.searchParams.get("offset");
64640
+ const requestedPage = rawLimit !== null || rawOffset !== null || url.searchParams.has("include_archived");
64641
+ const limit = rawLimit === null ? 500 : Number(rawLimit);
64642
+ const offset = rawOffset === null ? 0 : Number(rawOffset);
64643
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
64644
+ return error(400, "agent limit must be an integer from 1 to 500");
64645
+ }
64646
+ if (!Number.isSafeInteger(offset) || offset < 0) {
64647
+ return error(400, "agent offset must be a non-negative integer");
64648
+ }
64649
+ const includeArchivedParam = url.searchParams.get("include_archived");
64650
+ if (includeArchivedParam !== null && includeArchivedParam !== "true" && includeArchivedParam !== "false") {
64651
+ return error(400, "include_archived must be true or false");
64652
+ }
64653
+ const page = await store.agents.listPage({
64654
+ limit,
64655
+ offset,
64656
+ include_archived: includeArchivedParam === "true"
64657
+ }, contextFromPrincipal(principal));
64658
+ if (!page || !Array.isArray(page.agents) || !Number.isSafeInteger(page.total) || page.total < 0 || page.agents.length > limit || page.agents.length > 0 && offset + page.agents.length > page.total) {
64659
+ return error(500, "agent storage returned an invalid bounded page");
64660
+ }
64661
+ const hasMore = offset + page.agents.length < page.total;
64662
+ if (!requestedPage && hasMore) {
64663
+ return error(426, "agent roster exceeds the bounded legacy window; send limit and offset with an upgraded client");
64664
+ }
64665
+ if (hasMore && page.agents.length === 0) {
64666
+ return error(500, "agent storage returned a stalled bounded page");
64667
+ }
64668
+ return json6({
64669
+ agents: page.agents,
64670
+ count: page.agents.length,
64671
+ total: page.total,
64672
+ limit,
64673
+ offset,
64674
+ has_more: hasMore,
64675
+ next_offset: hasMore ? offset + page.agents.length : null
64676
+ });
63116
64677
  }
63117
64678
  if (!id && method === "POST") {
63118
64679
  const body2 = await readJson3(req);
@@ -63157,9 +64718,15 @@ async function handleV1Request(req, url, dependencies = {}) {
63157
64718
  if (method !== "GET")
63158
64719
  return error(405, `method ${method} not allowed on /v1/activity`);
63159
64720
  const limitParam = url.searchParams.get("limit");
63160
- const limit = limitParam ? Math.max(1, Math.min(1e4, Number(limitParam) || 50)) : 50;
63161
- const activity = await store.audit.getRecentActivity(limit);
63162
- return json6({ activity, count: activity.length });
64721
+ const limit = limitParam === null ? 50 : Number(limitParam);
64722
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4) {
64723
+ return error(400, "activity limit must be an integer from 1 to 10000");
64724
+ }
64725
+ const activity = await store.audit.getRecentActivity(limit, contextFromPrincipal(principal));
64726
+ if (!Array.isArray(activity) || activity.length > limit) {
64727
+ return error(500, "activity storage returned an invalid bounded result");
64728
+ }
64729
+ return json6({ activity, count: activity.length, limit });
63163
64730
  }
63164
64731
  if (resource === "task-lists") {
63165
64732
  if (!id && method === "GET") {
@@ -63253,11 +64820,40 @@ async function handleV1Request(req, url, dependencies = {}) {
63253
64820
  if (resource === "dependencies" && !id) {
63254
64821
  if (method !== "GET")
63255
64822
  return error(405, `method ${method} not allowed on /v1/dependencies`);
63256
- if (typeof store.dependencies?.listAll !== "function") {
63257
- return error(501, "dependency edge listing is not supported by this storage backend");
63258
- }
63259
- const dependencies2 = await store.dependencies.listAll();
63260
- return json6({ dependencies: dependencies2, count: dependencies2.length });
64823
+ const limitParam = url.searchParams.get("limit");
64824
+ const offsetParam = url.searchParams.get("offset");
64825
+ const requestedPage = limitParam !== null || offsetParam !== null;
64826
+ const limit = limitParam === null ? 500 : Number(limitParam);
64827
+ const offset = offsetParam === null ? 0 : Number(offsetParam);
64828
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
64829
+ return error(400, "dependency limit must be an integer from 1 to 500");
64830
+ }
64831
+ if (!Number.isSafeInteger(offset) || offset < 0) {
64832
+ return error(400, "dependency offset must be a non-negative integer");
64833
+ }
64834
+ if (typeof store.dependencies?.listPage !== "function") {
64835
+ return error(501, "bounded dependency pagination is not supported by this storage backend");
64836
+ }
64837
+ const page = await store.dependencies.listPage({ limit, offset }, contextFromPrincipal(principal));
64838
+ if (!page || !Array.isArray(page.dependencies) || !Number.isSafeInteger(page.total) || page.total < 0 || page.dependencies.length > limit || offset + page.dependencies.length > page.total) {
64839
+ return error(500, "dependency storage returned an invalid bounded page");
64840
+ }
64841
+ const hasMore = offset + page.dependencies.length < page.total;
64842
+ if (!requestedPage && hasMore) {
64843
+ return error(426, "dependency graph exceeds the bounded legacy window; send limit and offset with an upgraded client");
64844
+ }
64845
+ if (hasMore && page.dependencies.length === 0) {
64846
+ return error(500, "dependency storage returned a stalled bounded page");
64847
+ }
64848
+ return json6({
64849
+ dependencies: page.dependencies,
64850
+ count: page.dependencies.length,
64851
+ total: page.total,
64852
+ limit,
64853
+ offset,
64854
+ has_more: hasMore,
64855
+ next_offset: hasMore ? offset + page.dependencies.length : null
64856
+ });
63261
64857
  }
63262
64858
  if (resource === "commits" && id) {
63263
64859
  if (method !== "GET")
@@ -63447,7 +65043,7 @@ async function handleV1Request(req, url, dependencies = {}) {
63447
65043
  return error(500, e.message || "internal error");
63448
65044
  }
63449
65045
  }
63450
- var JSON_HEADERS5, DEFAULT_COMMENT_PAGE_SIZE = 100, MAX_COMMENT_PAGE_SIZE = 500, LEGACY_COMMENT_RESPONSE_LIMIT = 500, RFC3339_DATE_TIME;
65046
+ var JSON_HEADERS5, DEFAULT_COMMENT_PAGE_SIZE = 100, MAX_COMMENT_PAGE_SIZE = 500, LEGACY_COMMENT_RESPONSE_LIMIT = 500, MAX_BULK_CREATE_TASKS = 50, MAX_BULK_DELETE_TASKS = 100, MAX_BULK_DEPENDENCIES = 100, RFC3339_DATE_TIME;
63451
65047
  var init_v1 = __esm(() => {
63452
65048
  init_atomic_project_migration();
63453
65049
  init_machine_registry();
@@ -64176,6 +65772,15 @@ function registerTaskCrudTools(server, ctx) {
64176
65772
  if (assigned_to) {
64177
65773
  resolved.assigned_to = resolveAssignee(await validateMcpAssignee(assigned_to, Boolean(params.allow_seat)));
64178
65774
  }
65775
+ const cloud = getTodosCloudClient();
65776
+ if (cloud) {
65777
+ if (project_id)
65778
+ resolved.project_id = await cloudResolveProjectRef(cloud, project_id);
65779
+ if (task_list_id)
65780
+ resolved.task_list_id = await cloudResolveTaskListRef(cloud, task_list_id);
65781
+ const remote = await cloudUpsertTaskByFingerprint(cloud, resolved);
65782
+ return { content: [{ type: "text", text: compactJson({ created: remote.created, task: JSON.parse(mutationTaskResponse(remote.task)) }) }] };
65783
+ }
64179
65784
  if (project_id)
64180
65785
  resolved.project_id = resolveId(project_id, "projects");
64181
65786
  if (task_list_id)
@@ -64744,7 +66349,7 @@ __export(exports_mention_resolver, {
64744
66349
  resolveMentions: () => resolveMentions
64745
66350
  });
64746
66351
  import { existsSync as existsSync20, readdirSync as readdirSync4, readFileSync as readFileSync14, statSync as statSync10 } from "fs";
64747
- import { basename as basename8, isAbsolute, join as join19, relative as relative5, resolve as resolve19, sep as sep4 } from "path";
66352
+ import { basename as basename8, isAbsolute as isAbsolute2, join as join19, relative as relative5, resolve as resolve19, sep as sep4 } from "path";
64748
66353
  function blankResolution(parsed) {
64749
66354
  return {
64750
66355
  input: parsed.input,
@@ -64771,7 +66376,7 @@ function normalizeWorkspace(workspace) {
64771
66376
  }
64772
66377
  function isInside(root, absolutePath) {
64773
66378
  const rel = relative5(root, absolutePath);
64774
- return rel === "" || !rel.startsWith("..") && !rel.includes(`..${sep4}`) && !isAbsolute(rel);
66379
+ return rel === "" || !rel.startsWith("..") && !rel.includes(`..${sep4}`) && !isAbsolute2(rel);
64775
66380
  }
64776
66381
  function normalizeRelativePath(value) {
64777
66382
  const normalized2 = value.trim().replace(/^\.?\//, "").replace(/\\/g, "/");
@@ -68834,6 +70439,12 @@ function registerTaskProjectTools(server, ctx) {
68834
70439
  version: exports_external.number().optional().describe("Expected version for optimistic locking")
68835
70440
  }, async ({ task_id, deadline, version: version2 }) => {
68836
70441
  try {
70442
+ const cloud = getTodosCloudClient();
70443
+ if (cloud) {
70444
+ const remoteTaskId = await cloudResolveTaskRef(cloud, task_id);
70445
+ const rescheduled = await cloudUpdateTaskVerified(cloud, remoteTaskId, version2 !== undefined ? { due_at: deadline, version: version2 } : { due_at: deadline });
70446
+ return { content: [{ type: "text", text: formatTask(rescheduled) }] };
70447
+ }
68837
70448
  const resolvedId = resolveId(task_id);
68838
70449
  const task2 = updateWithOptionalVersion(resolvedId, { due_at: deadline }, version2);
68839
70450
  return { content: [{ type: "text", text: formatTask(task2) }] };
@@ -68921,12 +70532,36 @@ function registerTaskProjectTools(server, ctx) {
68921
70532
  }
68922
70533
  if (shouldRegisterTool("bulk_update_tasks")) {
68923
70534
  server.tool("bulk_update_tasks", "Update multiple tasks at once. All tasks must pass the dependency check.", {
68924
- task_ids: exports_external.array(exports_external.string()).describe("Array of task IDs to update"),
70535
+ task_ids: exports_external.array(exports_external.string().min(1).max(200)).min(1).max(100).describe("Array of task IDs to update"),
68925
70536
  status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
68926
70537
  priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
68927
70538
  assigned_to: exports_external.string().nullable().optional().describe("Agent ID or name, null to unassign")
68928
70539
  }, async ({ task_ids, status: status3, priority, assigned_to }) => {
68929
70540
  try {
70541
+ const cloud = getTodosCloudClient();
70542
+ if (cloud) {
70543
+ const resolvedIds = [];
70544
+ for (const reference of task_ids)
70545
+ resolvedIds.push(await cloudResolveTaskRef(cloud, reference));
70546
+ const patch = {};
70547
+ if (status3 !== undefined)
70548
+ patch.status = status3;
70549
+ if (priority !== undefined)
70550
+ patch.priority = priority;
70551
+ if (assigned_to !== undefined)
70552
+ patch.assigned_to = assigned_to === null ? null : await cloudResolveAgentRef(cloud, assigned_to);
70553
+ let updated = 0;
70554
+ const failed = [];
70555
+ for (let index = 0;index < resolvedIds.length; index++) {
70556
+ try {
70557
+ await cloudUpdateTaskVerified(cloud, resolvedIds[index], patch);
70558
+ updated++;
70559
+ } catch {
70560
+ failed.push(task_ids[index]);
70561
+ }
70562
+ }
70563
+ return { content: [{ type: "text", text: `${updated} task(s) updated, ${failed.length} failed.${failed.length ? ` Failed: ${failed.map((f) => f.slice(0, 8)).join(", ")}` : ""}` }] };
70564
+ }
68930
70565
  const { bulkUpdateTasks: bulkUpdateTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
68931
70566
  const resolved = task_ids.map(resolveId);
68932
70567
  let resolvedAssignee = assigned_to;
@@ -68942,6 +70577,7 @@ function registerTaskProjectTools(server, ctx) {
68942
70577
  if (shouldRegisterTool("bulk_create_tasks")) {
68943
70578
  server.tool("bulk_create_tasks", "Create multiple tasks at once from an array of task objects.", {
68944
70579
  tasks: exports_external.array(exports_external.object({
70580
+ temp_id: exports_external.string().min(1).max(100).optional().describe("Caller-local handle for sibling dependencies in this batch"),
68945
70581
  title: exports_external.string(),
68946
70582
  description: exports_external.string().optional(),
68947
70583
  status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
@@ -68949,28 +70585,44 @@ function registerTaskProjectTools(server, ctx) {
68949
70585
  project_id: exports_external.string().optional(),
68950
70586
  task_list_id: exports_external.string().optional(),
68951
70587
  assigned_to: exports_external.string().optional(),
68952
- depends_on: exports_external.array(exports_external.string()).optional(),
68953
- short_id: exports_external.string().nullable().optional(),
70588
+ depends_on: exports_external.array(exports_external.string().min(1).max(200)).max(100).optional(),
68954
70589
  tags: exports_external.array(exports_external.string()).optional(),
68955
70590
  estimate: exports_external.number().optional()
68956
- })).describe("Array of task objects")
70591
+ })).min(1).max(50).describe("Array of task objects")
68957
70592
  }, async ({ tasks }) => {
68958
70593
  try {
68959
- const { bulkCreateTasks: bulkCreateTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
68960
- const resolved = tasks.map((t) => {
68961
- const r = { ...t };
68962
- if (r.project_id)
68963
- r.project_id = resolveId(r.project_id, "projects");
68964
- if (r.task_list_id)
68965
- r.task_list_id = resolveId(r.task_list_id, "task_lists");
68966
- if (r.assigned_to)
68967
- r.assigned_to = resolveId(r.assigned_to, "agents");
68968
- if (r.depends_on)
68969
- r.depends_on = r.depends_on.map((id) => resolveId(id));
68970
- return r;
68971
- });
68972
- const result = bulkCreateTasks2(resolved);
68973
- return { content: [{ type: "text", text: `${result.created.length} task(s) created.` }] };
70594
+ const router = resolveWritableIdentity();
70595
+ const cloud = getTodosCloudClient();
70596
+ if (cloud) {
70597
+ const remoteAgentId = router.agent_id ? await cloudResolveAgentRef(cloud, router.agent_id) : undefined;
70598
+ const remote = [];
70599
+ for (const t of tasks) {
70600
+ const { estimate, project_id, task_list_id, ...rest } = t;
70601
+ const assignedTo = t.assigned_to ? await cloudResolveAgentRef(cloud, t.assigned_to) : undefined;
70602
+ remote.push({
70603
+ ...rest,
70604
+ ...remoteAgentId ? { agent_id: remoteAgentId, created_by: remoteAgentId } : {},
70605
+ ...assignedTo ? { assigned_to: assignedTo } : {},
70606
+ ...estimate !== undefined ? { estimated_minutes: estimate } : {},
70607
+ ...project_id ? { project_id: await cloudResolveProjectRef(cloud, project_id) } : {},
70608
+ ...task_list_id ? { task_list_id: await cloudResolveTaskListRef(cloud, task_list_id) } : {}
70609
+ });
70610
+ }
70611
+ const receipt2 = await cloudBulkCreateTasks(cloud, remote);
70612
+ return { content: [{ type: "text", text: JSON.stringify(receipt2) }] };
70613
+ }
70614
+ const { createLocalSqliteTodosStorageAdapter: createLocalSqliteTodosStorageAdapter2 } = (init_local_sqlite(), __toCommonJS(exports_local_sqlite));
70615
+ const resolved = tasks.map((task2) => ({
70616
+ ...task2,
70617
+ ...router.agent_id ? { agent_id: router.agent_id, created_by: router.agent_id } : {},
70618
+ ...task2.project_id ? { project_id: resolveId(task2.project_id, "projects") } : {},
70619
+ ...task2.task_list_id ? { task_list_id: resolveId(task2.task_list_id, "task_lists") } : {},
70620
+ ...task2.assigned_to ? { assigned_to: resolveId(task2.assigned_to, "agents") } : {},
70621
+ ...task2.estimate !== undefined ? { estimated_minutes: task2.estimate } : {}
70622
+ }));
70623
+ const local = createLocalSqliteTodosStorageAdapter2({ db: getDatabase() });
70624
+ const receipt = await local.tasks.bulkCreateAtomic(resolved);
70625
+ return { content: [{ type: "text", text: `${receipt.created.length} task(s) created.` }] };
68974
70626
  } catch (e) {
68975
70627
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
68976
70628
  }
@@ -68978,14 +70630,21 @@ function registerTaskProjectTools(server, ctx) {
68978
70630
  }
68979
70631
  if (shouldRegisterTool("bulk_delete_tasks")) {
68980
70632
  server.tool("bulk_delete_tasks", "Delete multiple tasks at once. Tasks with active children are skipped.", {
68981
- task_ids: exports_external.array(exports_external.string()).describe("Array of task IDs"),
70633
+ task_ids: exports_external.array(exports_external.string().min(1).max(200)).min(1).max(100).describe("Array of unique task IDs or refs"),
68982
70634
  force: exports_external.boolean().optional().describe("Skip child check for all tasks (dangerous)")
68983
70635
  }, async ({ task_ids, force }) => {
68984
70636
  try {
68985
- const { bulkDeleteTasks: bulkDeleteTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
68986
- const resolved = task_ids.map(resolveId);
68987
- const result = bulkDeleteTasks2(resolved, force);
68988
- return { content: [{ type: "text", text: `${result.deleted} task(s) deleted, ${result.skipped} skipped (has children).` }] };
70637
+ const cloud = getTodosCloudClient();
70638
+ if (cloud) {
70639
+ const receipt2 = await cloudBulkDeleteTasks(cloud, task_ids, force === true);
70640
+ return { content: [{ type: "text", text: JSON.stringify(receipt2) }] };
70641
+ }
70642
+ const { createLocalSqliteTodosStorageAdapter: createLocalSqliteTodosStorageAdapter2 } = (init_local_sqlite(), __toCommonJS(exports_local_sqlite));
70643
+ const local = createLocalSqliteTodosStorageAdapter2({ db: getDatabase() });
70644
+ const receipt = await local.tasks.bulkDeleteAtomic(task_ids, force === true);
70645
+ const deleted = receipt.results.filter((result) => result.outcome === "deleted").length;
70646
+ const skipped = receipt.results.filter((result) => result.outcome === "skipped").length;
70647
+ return { content: [{ type: "text", text: `${deleted} task(s) deleted, ${skipped} skipped (has children).` }] };
68989
70648
  } catch (e) {
68990
70649
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
68991
70650
  }
@@ -69921,6 +71580,20 @@ Tasks:` : null,
69921
71580
  task_id: exports_external.string().describe("Task ID")
69922
71581
  }, async ({ task_id }) => {
69923
71582
  try {
71583
+ const cloud = getTodosCloudClient();
71584
+ if (cloud) {
71585
+ const remoteTaskId = await cloudResolveTaskRef(cloud, task_id);
71586
+ const page = await cloudListComments(cloud, remoteTaskId, { limit: 100 });
71587
+ if (page.comments.length === 0)
71588
+ return { content: [{ type: "text", text: "No comments." }] };
71589
+ const remoteLines = page.comments.map((c) => `[${c.agent_id || "unknown"}] ${c.created_at?.slice(0, 16)}:
71590
+ ${c.content}`);
71591
+ if (page.has_more)
71592
+ remoteLines.push(`(older comments available; showing the ${page.comments.length} most recent)`);
71593
+ return { content: [{ type: "text", text: remoteLines.join(`
71594
+
71595
+ `) }] };
71596
+ }
69924
71597
  const resolvedId = resolveId(task_id);
69925
71598
  const comments = listComments(resolvedId);
69926
71599
  if (comments.length === 0)
@@ -69939,13 +71612,50 @@ Tasks:` : null,
69939
71612
  server.tool("get_activity_timeline", "Get a unified local activity timeline across comments, task history, and run evidence.", {
69940
71613
  entity_type: exports_external.enum(["all", "task", "project", "plan", "run"]).optional().describe("Scope type. Defaults to all."),
69941
71614
  entity_id: exports_external.string().optional().describe("ID for task/project/plan/run scope."),
69942
- limit: exports_external.number().optional().describe("Max entries, default 50."),
69943
- offset: exports_external.number().optional().describe("Entries to skip for pagination."),
71615
+ limit: exports_external.number().int().min(1).max(200).optional().describe("Max entries, default 50, maximum 200."),
71616
+ offset: exports_external.number().int().min(0).max(1e4).optional().describe("Entries to skip for pagination, maximum 10000."),
69944
71617
  order: exports_external.enum(["asc", "desc"]).optional().describe("Sort order, default desc."),
69945
71618
  since: exports_external.string().optional().describe("Only entries at or after this ISO timestamp."),
69946
71619
  until: exports_external.string().optional().describe("Only entries at or before this ISO timestamp.")
69947
71620
  }, async (input) => {
69948
71621
  try {
71622
+ const cloud = getTodosCloudClient();
71623
+ if (cloud) {
71624
+ const limit = input.limit ?? 50;
71625
+ const offset = input.offset ?? 0;
71626
+ const scopeType = input.entity_type ?? "all";
71627
+ if (scopeType !== "all" && scopeType !== "task") {
71628
+ throw new Error(`REMOTE_API_INCOMPATIBLE: hosted activity does not yet support ${scopeType}-scoped timelines`);
71629
+ }
71630
+ const entityId2 = scopeType === "task" ? input.entity_id ? await cloudResolveTaskRef(cloud, input.entity_id) : null : null;
71631
+ const historyPage = entityId2 ? await cloudTaskHistoryPage(cloud, entityId2, {
71632
+ limit,
71633
+ offset,
71634
+ order: input.order ?? "desc",
71635
+ ...input.since ? { since: input.since } : {},
71636
+ ...input.until ? { until: input.until } : {}
71637
+ }) : null;
71638
+ let entries = historyPage ? historyPage.history : await cloudRecentActivity(cloud, Math.min(1e4, limit + offset));
71639
+ if (!historyPage) {
71640
+ if (input.since)
71641
+ entries = entries.filter((entry2) => (entry2.created_at ?? "") >= input.since);
71642
+ if (input.until)
71643
+ entries = entries.filter((entry2) => (entry2.created_at ?? "") <= input.until);
71644
+ }
71645
+ if (!historyPage && input.order === "asc")
71646
+ entries = [...entries].reverse();
71647
+ const page = historyPage ? entries : entries.slice(offset, offset + limit);
71648
+ return { content: [{ type: "text", text: JSON.stringify({
71649
+ source: "cloud",
71650
+ scope: { entity_type: scopeType, entity_id: entityId2 },
71651
+ entries: page,
71652
+ count: page.length,
71653
+ complete: historyPage ? offset === 0 && !historyPage.has_more : false,
71654
+ ...historyPage ? { total: historyPage.total, has_more: historyPage.has_more, next_offset: historyPage.next_offset } : {},
71655
+ omitted_sources: entityId2 ? ["comments", "run_evidence"] : ["comments", "run_evidence", "older_task_history"],
71656
+ omitted_reason: entityId2 ? "the hosted task-history route does not include comments or run evidence" : "the hosted recent-activity route is bounded and does not include comments or run evidence"
71657
+ }) }] };
71658
+ }
69949
71659
  let entityId = input.entity_id;
69950
71660
  if (input.entity_type === "task" && entityId)
69951
71661
  entityId = resolveId(entityId);
@@ -70163,9 +71873,25 @@ Tasks:` : null,
70163
71873
  query: exports_external.string().describe("Search query"),
70164
71874
  project_id: exports_external.string().optional().describe("Filter by project"),
70165
71875
  status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
70166
- limit: exports_external.number().optional().describe("Max results (default: 20)")
71876
+ limit: exports_external.number().int().min(1).max(100).optional().describe("Max results (default: 20, maximum: 100)")
70167
71877
  }, async ({ query, project_id, status: status3, limit }) => {
70168
71878
  try {
71879
+ const cloud = getTodosCloudClient();
71880
+ if (cloud) {
71881
+ const remoteProjectId = project_id ? await cloudResolveProjectRef(cloud, project_id) : undefined;
71882
+ const remote = await cloudListTasks(cloud, {
71883
+ query,
71884
+ ...remoteProjectId ? { project_id: remoteProjectId } : {},
71885
+ ...status3 ? { status: status3 } : {},
71886
+ limit: limit || 20
71887
+ });
71888
+ if (remote.length === 0)
71889
+ return { content: [{ type: "text", text: `No results for: ${query}` }] };
71890
+ const remoteLines = remote.map((t) => `${t.short_id || t.id.slice(0, 8)} [${t.status}] ${t.title}`);
71891
+ return { content: [{ type: "text", text: `${remote.length} result(s) for "${query}":
71892
+ ${remoteLines.join(`
71893
+ `)}` }] };
71894
+ }
70169
71895
  const { searchTasks: searchTasks2 } = (init_search(), __toCommonJS(exports_search));
70170
71896
  const resolved = { query, limit };
70171
71897
  if (project_id)
@@ -70266,6 +71992,8 @@ var init_task_project_tools = __esm(() => {
70266
71992
  init_comments();
70267
71993
  init_task_runs();
70268
71994
  init_project_bootstrap();
71995
+ init_database();
71996
+ init_creator_identity();
70269
71997
  init_labels();
70270
71998
  init_tags();
70271
71999
  init_redaction();
@@ -71068,6 +72796,29 @@ function registerTaskAutoTools(server, ctx) {
71068
72796
  project_id: exports_external.string().optional().describe("Scope to a project")
71069
72797
  }, async ({ days = 7, project_id }) => {
71070
72798
  try {
72799
+ const cloud = getTodosCloudClient();
72800
+ if (cloud) {
72801
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
72802
+ const stampedAt = new Date().toISOString();
72803
+ const remoteProjectId = project_id ? await cloudResolveProjectRef(cloud, project_id) : undefined;
72804
+ const selection = await cloudListTasksCompleteBounded(cloud, {
72805
+ status: "completed",
72806
+ include_archived: false,
72807
+ ...remoteProjectId ? { project_id: remoteProjectId } : {}
72808
+ });
72809
+ for (const task2 of selection) {
72810
+ if (typeof task2.updated_at !== "string" || !task2.updated_at || !Number.isSafeInteger(task2.version)) {
72811
+ throw new Error("REMOTE_API_INCOMPATIBLE: archive selection contains a task without updated_at and version");
72812
+ }
72813
+ }
72814
+ const candidates = selection.filter((task2) => task2.updated_at < cutoff);
72815
+ let archived = 0;
72816
+ for (const t of candidates) {
72817
+ await cloudUpdateTaskVerified(cloud, t.id, { archived_at: stampedAt, version: t.version });
72818
+ archived++;
72819
+ }
72820
+ return { content: [{ type: "text", text: `Archived ${archived} completed task(s) older than ${days} days.` }] };
72821
+ }
71071
72822
  const { archiveCompletedTasks: archiveCompletedTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
71072
72823
  const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
71073
72824
  const count = archiveCompletedTasks2(days, resolvedProjectId);
@@ -71082,6 +72833,14 @@ function registerTaskAutoTools(server, ctx) {
71082
72833
  task_id: exports_external.string().describe("Task ID")
71083
72834
  }, async ({ task_id }) => {
71084
72835
  try {
72836
+ const cloud = getTodosCloudClient();
72837
+ if (cloud) {
72838
+ const current = await cloudGetTask(cloud, task_id);
72839
+ if (!current)
72840
+ throw new Error(`Task not found: ${task_id}`);
72841
+ await cloudUpdateTaskVerified(cloud, current.id, { archived_at: null, version: current.version });
72842
+ return { content: [{ type: "text", text: `Task ${task_id.slice(0, 8)} restored from archive.` }] };
72843
+ }
71085
72844
  const { unarchiveTask: unarchiveTask2 } = (init_tasks(), __toCommonJS(exports_tasks));
71086
72845
  unarchiveTask2(resolveId(task_id));
71087
72846
  return { content: [{ type: "text", text: `Task ${task_id.slice(0, 8)} restored from archive.` }] };
@@ -71093,12 +72852,36 @@ function registerTaskAutoTools(server, ctx) {
71093
72852
  if (shouldRegisterTool("get_archived_tasks")) {
71094
72853
  server.tool("get_archived_tasks", "List archived tasks.", {
71095
72854
  project_id: exports_external.string().optional().describe("Filter by project"),
71096
- limit: exports_external.number().optional().describe("Max results (default: 50)")
71097
- }, async ({ project_id, limit }) => {
72855
+ limit: exports_external.number().int().min(1).max(200).optional().describe("Max results (default: 50, maximum: 200)"),
72856
+ offset: exports_external.number().int().min(0).max(1e4).optional().describe("Archived rows to skip (default: 0, maximum: 10000)")
72857
+ }, async ({ project_id, limit, offset }) => {
71098
72858
  try {
71099
- const { getArchivedTasks: getArchivedTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
72859
+ const cloud = getTodosCloudClient();
72860
+ if (cloud) {
72861
+ const remoteProjectId = project_id ? await cloudResolveProjectRef(cloud, project_id) : undefined;
72862
+ const page = await cloudListArchivedTasksPage(cloud, {
72863
+ ...remoteProjectId ? { project_id: remoteProjectId } : {},
72864
+ limit: limit ?? 50,
72865
+ offset: offset ?? 0
72866
+ });
72867
+ if (page.tasks.length === 0)
72868
+ return { content: [{ type: "text", text: "No archived tasks." }] };
72869
+ const remoteLines = page.tasks.map((t) => `${t.short_id || t.id.slice(0, 8)} ${t.title} archived ${t.archived_at}`);
72870
+ if (page.has_more)
72871
+ remoteLines.push(`(${page.total - (page.offset + page.tasks.length)} more archived task(s); next offset ${page.next_offset})`);
72872
+ return { content: [{ type: "text", text: remoteLines.join(`
72873
+ `) }] };
72874
+ }
72875
+ const { listTasks: listTasks3 } = (init_tasks(), __toCommonJS(exports_tasks));
71100
72876
  const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
71101
- const tasks = getArchivedTasks2({ project_id: resolvedProjectId, limit: limit || 50 });
72877
+ const tasks = listTasks3({
72878
+ ...resolvedProjectId ? { project_id: resolvedProjectId } : {},
72879
+ include_archived: true,
72880
+ archived_only: true,
72881
+ include_subtasks: true,
72882
+ limit: limit ?? 50,
72883
+ offset: offset ?? 0
72884
+ });
71102
72885
  if (tasks.length === 0)
71103
72886
  return { content: [{ type: "text", text: "No archived tasks." }] };
71104
72887
  const lines = tasks.map((t) => `${t.short_id || t.id.slice(0, 8)} ${t.title} archived ${t.archived_at}`);
@@ -71131,12 +72914,29 @@ function registerTaskAutoTools(server, ctx) {
71131
72914
  agent_id: exports_external.string().optional().describe("Agent ID (defaults to context agent)")
71132
72915
  }, async ({ agent_id }) => {
71133
72916
  try {
71134
- const { listTasks: listTasks3, getBlockedTasks: getBlockedTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
71135
72917
  const focus = ctx.getAgentFocus(agent_id || "");
71136
72918
  const effectiveAgentId = focus ? focus.agent_id : agent_id || "";
71137
72919
  if (!effectiveAgentId) {
71138
72920
  return { content: [{ type: "text", text: "No agent_id provided and no agent focus is active." }], isError: true };
71139
72921
  }
72922
+ const cloud = getTodosCloudClient();
72923
+ if (cloud) {
72924
+ const remoteAssigned = await cloudListTasks(cloud, { assigned_to: effectiveAgentId, limit: 500 });
72925
+ const remoteNow = Date.now();
72926
+ const remoteDueSoon = remoteNow + 24 * 60 * 60 * 1000;
72927
+ const remoteBlocked = await cloudBlockingDepsMap(cloud, remoteAssigned.filter((t) => !["completed", "cancelled", "failed"].includes(t.status)));
72928
+ const remoteLines = [
72929
+ `Agent: ${effectiveAgentId}`,
72930
+ `In Progress: ${remoteAssigned.filter((t) => t.status === "in_progress").length}`,
72931
+ `Pending: ${remoteAssigned.filter((t) => t.status === "pending").length}`,
72932
+ `Completed (recent): ${remoteAssigned.filter((t) => t.status === "completed" && t.completed_at && remoteNow - new Date(t.completed_at).getTime() <= 7 * 24 * 60 * 60 * 1000).length}`,
72933
+ `Due Soon: ${remoteAssigned.filter((t) => t.due_at && new Date(t.due_at).getTime() <= remoteDueSoon && new Date(t.due_at).getTime() >= remoteNow && !["completed", "cancelled", "failed"].includes(t.status)).length}`,
72934
+ `Blocked: ${remoteBlocked.size}`
72935
+ ];
72936
+ return { content: [{ type: "text", text: remoteLines.join(`
72937
+ `) }] };
72938
+ }
72939
+ const { listTasks: listTasks3, getBlockedTasks: getBlockedTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
71140
72940
  const assigned = listTasks3({ assigned_to: effectiveAgentId, limit: 500 }, undefined);
71141
72941
  const now4 = Date.now();
71142
72942
  const dueSoonCutoff = now4 + 24 * 60 * 60 * 1000;
@@ -71170,6 +72970,60 @@ function registerTaskAutoTools(server, ctx) {
71170
72970
  max_per_agent: exports_external.number().optional().describe("Max tasks per agent (default: 5)")
71171
72971
  }, async ({ project_id, max_per_agent }) => {
71172
72972
  try {
72973
+ const cloud = getTodosCloudClient();
72974
+ if (cloud) {
72975
+ const cap = max_per_agent || 5;
72976
+ const remoteProjectId = project_id ? await cloudResolveProjectRef(cloud, project_id) : undefined;
72977
+ const roster = (await cloudListAgents(cloud)).filter((a) => a.status !== "archived");
72978
+ if (roster.length === 0)
72979
+ return { content: [{ type: "text", text: "No active agents available for rebalancing." }] };
72980
+ const active = await cloudListTasksCompleteBounded(cloud, {
72981
+ status: ["pending", "in_progress"],
72982
+ ...remoteProjectId ? { project_id: remoteProjectId } : {}
72983
+ });
72984
+ for (const task2 of active) {
72985
+ if (!Number.isSafeInteger(task2.version) || typeof task2.status !== "string") {
72986
+ throw new Error("REMOTE_API_INCOMPATIBLE: rebalance selection contains an incomplete task");
72987
+ }
72988
+ }
72989
+ const keyByAlias = new Map;
72990
+ const ambiguous = new Set;
72991
+ const index = (alias2, agentId) => {
72992
+ if (!alias2)
72993
+ return;
72994
+ const key3 = alias2.toLowerCase();
72995
+ if (ambiguous.has(key3))
72996
+ return;
72997
+ const seen = keyByAlias.get(key3);
72998
+ if (seen !== undefined && seen !== agentId) {
72999
+ keyByAlias.delete(key3);
73000
+ ambiguous.add(key3);
73001
+ return;
73002
+ }
73003
+ keyByAlias.set(key3, agentId);
73004
+ };
73005
+ for (const agent of roster) {
73006
+ index(agent.id, agent.id);
73007
+ index(agent.name, agent.id);
73008
+ }
73009
+ const keyOf = (assignedTo) => assignedTo ? keyByAlias.get(assignedTo.toLowerCase()) : undefined;
73010
+ const load2 = new Map(roster.map((a) => [a.id, active.filter((t) => keyOf(t.assigned_to) === a.id).length]));
73011
+ let moved2 = 0;
73012
+ let skipped2 = 0;
73013
+ for (const t of active.filter((t2) => t2.status === "pending" && keyOf(t2.assigned_to) && (load2.get(keyOf(t2.assigned_to)) ?? 0) > cap)) {
73014
+ const from = keyOf(t.assigned_to);
73015
+ const target = roster.filter((a) => a.id !== from).sort((a, b) => (load2.get(a.id) ?? 0) - (load2.get(b.id) ?? 0))[0];
73016
+ if (!target || (load2.get(target.id) ?? 0) >= cap) {
73017
+ skipped2++;
73018
+ continue;
73019
+ }
73020
+ await cloudUpdateTaskVerified(cloud, t.id, { assigned_to: target.id, version: t.version });
73021
+ load2.set(from, (load2.get(from) ?? 1) - 1);
73022
+ load2.set(target.id, (load2.get(target.id) ?? 0) + 1);
73023
+ moved2++;
73024
+ }
73025
+ return { content: [{ type: "text", text: `Rebalanced: moved ${moved2} task(s), ${skipped2} skipped.` }] };
73026
+ }
71173
73027
  const { listAgents: listAgents2 } = (init_agents(), __toCommonJS(exports_agents));
71174
73028
  const { listTasks: listTasks3, updateTask: updateTask3 } = (init_tasks(), __toCommonJS(exports_tasks));
71175
73029
  const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
@@ -71226,6 +73080,20 @@ function registerTaskAutoTools(server, ctx) {
71226
73080
  agent_id: exports_external.string().optional().describe("Filter by assignee")
71227
73081
  }, async ({ hours = 24, project_id, agent_id }) => {
71228
73082
  try {
73083
+ const cloud = getTodosCloudClient();
73084
+ if (cloud) {
73085
+ const horizon = Date.now() + hours * 60 * 60 * 1000;
73086
+ const remote = (await cloudListTasks(cloud, {
73087
+ ...project_id ? { project_id } : {},
73088
+ ...agent_id ? { assigned_to: agent_id } : {}
73089
+ })).filter((t) => t.due_at && !["completed", "cancelled", "failed"].includes(t.status) && new Date(t.due_at).getTime() <= horizon && new Date(t.due_at).getTime() >= Date.now()).sort((a, b) => (a.due_at ?? "").localeCompare(b.due_at ?? ""));
73090
+ if (remote.length === 0)
73091
+ return { content: [{ type: "text", text: "No deadlines approaching." }] };
73092
+ const remoteLines = remote.map((t) => `${t.short_id || t.id.slice(0, 8)} ${t.title} due ${t.due_at}`);
73093
+ return { content: [{ type: "text", text: `${remote.length} task(s) due within ${hours}h:
73094
+ ${remoteLines.join(`
73095
+ `)}` }] };
73096
+ }
71229
73097
  const { notifyUpcomingDeadlines: notifyUpcomingDeadlines2 } = (init_tasks(), __toCommonJS(exports_tasks));
71230
73098
  const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
71231
73099
  const resolvedAgentId = agent_id ? resolveId(agent_id, "agents") : undefined;
@@ -71248,6 +73116,16 @@ ${lines.join(`
71248
73116
  limit: exports_external.number().optional().describe("Max results (default: 50)")
71249
73117
  }, async ({ project_id, agent_id, limit }) => {
71250
73118
  try {
73119
+ const cloud = getTodosCloudClient();
73120
+ if (cloud) {
73121
+ const remote = (await cloudEscalatedTasks(cloud, { project_id, agent_id })).slice(0, limit || 50);
73122
+ if (remote.length === 0)
73123
+ return { content: [{ type: "text", text: "No SLA breaches or overdue tasks." }] };
73124
+ const remoteLines = remote.map((item) => `${item.task.short_id || item.task.id.slice(0, 8)} ${item.task.title} ${item.reasons.join(",")} breached ${item.breached_at}`);
73125
+ return { content: [{ type: "text", text: `${remote.length} escalation(s):
73126
+ ${remoteLines.join(`
73127
+ `)}` }] };
73128
+ }
71251
73129
  const { getEscalatedTasks: getEscalatedTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
71252
73130
  const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
71253
73131
  const resolvedAgentId = agent_id ? resolveId(agent_id, "agents") : undefined;
@@ -71273,6 +73151,16 @@ ${lines.join(`
71273
73151
  project_id: exports_external.string().optional().describe("Filter by project")
71274
73152
  }, async ({ hours = 48, minutes: minutes2, project_id }) => {
71275
73153
  try {
73154
+ const cloud = getTodosCloudClient();
73155
+ if (cloud) {
73156
+ const remote = await cloudStaleTasks(cloud, minutes2 ?? hours * 60, project_id ? { project_id } : {});
73157
+ if (remote.length === 0)
73158
+ return { content: [{ type: "text", text: "No stale tasks." }] };
73159
+ const remoteLines = remote.map((t) => `${t.short_id || t.id.slice(0, 8)} [${t.status}] ${t.title} \u2014 last updated ${t.updated_at}`);
73160
+ return { content: [{ type: "text", text: `${remote.length} stale task(s):
73161
+ ${remoteLines.join(`
73162
+ `)}` }] };
73163
+ }
71276
73164
  const { getStaleTasks: getStaleTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
71277
73165
  const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
71278
73166
  const tasks = getStaleTasks2({ hours, minutes: minutes2, project_id: resolvedProjectId });
@@ -71292,6 +73180,18 @@ ${lines.join(`
71292
73180
  project_id: exports_external.string().optional().describe("Filter by project")
71293
73181
  }, async ({ project_id }) => {
71294
73182
  try {
73183
+ const cloud = getTodosCloudClient();
73184
+ if (cloud) {
73185
+ const candidates = (await cloudListTasks(cloud, project_id ? { project_id } : {})).filter((t) => !["completed", "cancelled"].includes(t.status));
73186
+ const blocking = await cloudBlockingDepsMap(cloud, candidates);
73187
+ const remote = candidates.filter((t) => blocking.has(t.id));
73188
+ if (remote.length === 0)
73189
+ return { content: [{ type: "text", text: "No blocked tasks." }] };
73190
+ const remoteLines = remote.map((t) => `${t.short_id || t.id.slice(0, 8)} [${t.status}] ${t.title} \u2014 blocked by ${(blocking.get(t.id) ?? []).map((b) => b.id.slice(0, 8)).join(", ")}`);
73191
+ return { content: [{ type: "text", text: `${remote.length} blocked task(s):
73192
+ ${remoteLines.join(`
73193
+ `)}` }] };
73194
+ }
71295
73195
  const { getBlockedTasks: getBlockedTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
71296
73196
  const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
71297
73197
  const tasks = getBlockedTasks2(resolvedProjectId);
@@ -71311,6 +73211,24 @@ ${lines.join(`
71311
73211
  project_id: exports_external.string().optional().describe("Filter by project")
71312
73212
  }, async ({ project_id }) => {
71313
73213
  try {
73214
+ const cloud = getTodosCloudClient();
73215
+ if (cloud) {
73216
+ const edges = await cloudAllDependencies(cloud);
73217
+ const dependentCount = new Map;
73218
+ for (const edge of edges) {
73219
+ if (!edge.task_id || !edge.depends_on)
73220
+ continue;
73221
+ dependentCount.set(edge.depends_on, (dependentCount.get(edge.depends_on) ?? 0) + 1);
73222
+ }
73223
+ const blockers = await cloudGetTasksByIds(cloud, [...dependentCount.keys()]);
73224
+ const remote = [...blockers.values()].filter((t) => !["completed", "cancelled"].includes(t.status)).filter((t) => !project_id || t.project_id === project_id).sort((a, b) => (dependentCount.get(b.id) ?? 0) - (dependentCount.get(a.id) ?? 0));
73225
+ if (remote.length === 0)
73226
+ return { content: [{ type: "text", text: "No tasks blocking others." }] };
73227
+ const remoteLines = remote.map((t) => `${t.short_id || t.id.slice(0, 8)} [${t.status}] ${t.title} \u2014 blocking ${dependentCount.get(t.id) ?? 0} task(s)`);
73228
+ return { content: [{ type: "text", text: `${remote.length} blocking task(s):
73229
+ ${remoteLines.join(`
73230
+ `)}` }] };
73231
+ }
71314
73232
  const { getBlockingTasks: getBlockingTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
71315
73233
  const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
71316
73234
  const tasks = getBlockingTasks2(resolvedProjectId);
@@ -71376,6 +73294,16 @@ ${lines.join(`
71376
73294
  apply: exports_external.boolean().optional().describe("Apply safe repairs. Defaults to false/dry-run.")
71377
73295
  }, async ({ apply }) => {
71378
73296
  try {
73297
+ const cloud = getTodosCloudClient();
73298
+ if (cloud) {
73299
+ if (apply) {
73300
+ return { content: [{ type: "text", text: "run_doctor --apply is not available on the hosted authority: repairs are server-side. Re-run without apply for the hosted integrity report." }], isError: true };
73301
+ }
73302
+ const report = await cloudGetIntegrityReport(cloud);
73303
+ if (!report)
73304
+ throw new Error("The configured Todos authority returned no integrity report");
73305
+ return { content: [{ type: "text", text: JSON.stringify({ source: "cloud", integrity: report }, null, 2) }] };
73306
+ }
71379
73307
  const { runTodosDoctor: runTodosDoctor2 } = (init_doctor(), __toCommonJS(exports_doctor));
71380
73308
  const result = runTodosDoctor2({ apply: Boolean(apply) });
71381
73309
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -72548,10 +74476,35 @@ ${JSON.stringify(task2.metadata, null, 2)}` : null
72548
74476
  project_id: exports_external.string().optional().describe("Filter by project")
72549
74477
  }, async ({ agent_id, project_id }) => {
72550
74478
  try {
72551
- const { listTasks: listTasks3 } = (init_tasks(), __toCommonJS(exports_tasks));
72552
74479
  const focus = ctx.getAgentFocus(agent_id || "");
72553
74480
  const effectiveAgentId = focus ? focus.agent_id : agent_id || "";
72554
74481
  const effectiveProjectId = focus?.project_id || project_id;
74482
+ const cloud = getTodosCloudClient();
74483
+ if (cloud) {
74484
+ const recap = await cloudRecap(cloud, 24, effectiveProjectId);
74485
+ const mine = (rows) => effectiveAgentId ? rows.filter((t) => (t.assigned_to ?? "").toLowerCase() === effectiveAgentId.toLowerCase()) : rows;
74486
+ const remoteInProgress = mine(recap.in_progress);
74487
+ const remoteCompleted = mine(recap.completed);
74488
+ const remoteBlocked = mine(recap.blocked);
74489
+ const remoteLines = [
74490
+ `Standup for ${effectiveAgentId} (${effectiveProjectId ? `project: ${effectiveProjectId.slice(0, 8)}` : "all projects"})`,
74491
+ remoteInProgress.length > 0 ? `
74492
+ In Progress (${remoteInProgress.length}):` : `
74493
+ No tasks in progress.`,
74494
+ ...remoteInProgress.map((t) => ` - ${t.title} (${t.id.slice(0, 8)})`),
74495
+ remoteCompleted.length > 0 ? `
74496
+ Completed in the last 24h (${remoteCompleted.length}):` : `
74497
+ No tasks completed in the last 24h.`,
74498
+ ...remoteCompleted.map((t) => ` - ${t.title} (${t.id.slice(0, 8)})`),
74499
+ remoteBlocked.length > 0 ? `
74500
+ Blocked (${remoteBlocked.length}):` : `
74501
+ No blocked tasks.`,
74502
+ ...remoteBlocked.map((t) => ` - ${t.title} (${t.id.slice(0, 8)})`)
74503
+ ].filter(Boolean);
74504
+ return { content: [{ type: "text", text: remoteLines.join(`
74505
+ `) }] };
74506
+ }
74507
+ const { listTasks: listTasks3 } = (init_tasks(), __toCommonJS(exports_tasks));
72555
74508
  const inProgress = listTasks3({
72556
74509
  assigned_to: effectiveAgentId,
72557
74510
  status: "in_progress",
@@ -72595,13 +74548,25 @@ No blocked tasks.`,
72595
74548
  if (shouldRegisterTool("claim_task")) {
72596
74549
  server.tool("claim_task", "Alias for start_task \u2014 mark a task as in_progress and assign it to the calling agent.", {
72597
74550
  task_id: exports_external.string().describe("Task ID"),
72598
- agent_id: exports_external.string().optional().describe("Agent claiming (defaults to context)")
74551
+ agent_id: exports_external.string().describe("Exact agent ID or configured focus identity claiming the task")
72599
74552
  }, async ({ task_id, agent_id }) => {
72600
74553
  try {
74554
+ const requestedAgent = agent_id?.trim();
74555
+ if (!requestedAgent) {
74556
+ return {
74557
+ content: [{ type: "text", text: "claim_task requires agent_id; the placeholder identity 'mcp' is never substituted." }],
74558
+ isError: true
74559
+ };
74560
+ }
74561
+ const focus = ctx.getAgentFocus(requestedAgent);
74562
+ const effectiveAgent = focus?.agent_id || requestedAgent;
74563
+ const cloud = getTodosCloudClient();
74564
+ if (cloud) {
74565
+ const claimed = await cloudTaskAction(cloud, task_id, "start", { agent_id: effectiveAgent });
74566
+ return { content: [{ type: "text", text: formatTask(claimed) }] };
74567
+ }
72601
74568
  const { startTask: startTask3 } = (init_tasks(), __toCommonJS(exports_tasks));
72602
74569
  const resolvedId = resolveId(task_id);
72603
- const focus = ctx.getAgentFocus(agent_id || "");
72604
- const effectiveAgent = focus ? focus.agent_id : agent_id || "mcp";
72605
74570
  const task2 = startTask3(resolvedId, effectiveAgent);
72606
74571
  return { content: [{ type: "text", text: formatTask(task2) }] };
72607
74572
  } catch (e) {
@@ -72614,6 +74579,14 @@ No blocked tasks.`,
72614
74579
  task_id: exports_external.string().describe("Task ID")
72615
74580
  }, async ({ task_id }) => {
72616
74581
  try {
74582
+ const cloud = getTodosCloudClient();
74583
+ if (cloud) {
74584
+ const current2 = await cloudGetTask(cloud, task_id);
74585
+ if (!current2)
74586
+ throw new Error(`Task not found: ${task_id}`);
74587
+ const released = await cloudUpdateTask(cloud, current2.id, { status: "pending", assigned_to: null, version: current2.version });
74588
+ return { content: [{ type: "text", text: formatTask(released) }] };
74589
+ }
72617
74590
  const { getTask: getTask2, updateTask: updateTask3 } = (init_tasks(), __toCommonJS(exports_tasks));
72618
74591
  const resolvedId = resolveId(task_id);
72619
74592
  const current = getTask2(resolvedId);
@@ -72706,6 +74679,15 @@ No blocked tasks.`,
72706
74679
  minutes: exports_external.number().describe("Additional minutes to add to estimate")
72707
74680
  }, async ({ task_id, minutes: minutes2 }) => {
72708
74681
  try {
74682
+ const cloud = getTodosCloudClient();
74683
+ if (cloud) {
74684
+ const current = await cloudGetTask(cloud, task_id);
74685
+ if (!current)
74686
+ throw new Error(`Task not found: ${task_id}`);
74687
+ const before = current.estimated_minutes || 0;
74688
+ const after = await cloudUpdateTask(cloud, current.id, { estimated_minutes: before + minutes2, version: current.version });
74689
+ return { content: [{ type: "text", text: `Estimate updated: ${before} \u2192 ${after.estimated_minutes} min (+${minutes2})` }] };
74690
+ }
72709
74691
  const { getTask: getTask2, updateTask: updateTask3 } = (init_tasks(), __toCommonJS(exports_tasks));
72710
74692
  const resolvedId = resolveId(task_id);
72711
74693
  const task2 = getTask2(resolvedId);
@@ -72748,6 +74730,18 @@ No blocked tasks.`,
72748
74730
  limit: exports_external.number().optional().describe("Max recent comments in compact mode (default: 20)")
72749
74731
  }, async ({ task_id, detail, limit }) => {
72750
74732
  try {
74733
+ const cloud = getTodosCloudClient();
74734
+ if (cloud) {
74735
+ const page = await cloudListComments(cloud, task_id, { limit: detail === "full" ? 500 : limit || 20 });
74736
+ if (page.comments.length === 0)
74737
+ return { content: [{ type: "text", text: "No comments." }] };
74738
+ const remoteLines = page.comments.map((c) => `[${c.agent_id || "?"}] ${c.created_at?.slice(0, 16)}: ${detail === "full" ? c.content : truncateText(c.content, 180)}`);
74739
+ if (page.has_more)
74740
+ remoteLines.unshift(`Showing ${page.comments.length} comment(s); older pages available.`);
74741
+ return { content: [{ type: "text", text: remoteLines.join(`
74742
+
74743
+ `) }] };
74744
+ }
72751
74745
  const { listComments: listComments2 } = (init_comments(), __toCommonJS(exports_comments));
72752
74746
  const comments = listComments2(resolveId(task_id));
72753
74747
  if (comments.length === 0)
@@ -72772,10 +74766,23 @@ No blocked tasks.`,
72772
74766
  limit: exports_external.number().optional()
72773
74767
  }, async ({ agent_id, status: status3, project_id, limit }) => {
72774
74768
  try {
72775
- const { listTasks: listTasks3 } = (init_tasks(), __toCommonJS(exports_tasks));
72776
74769
  const focus = ctx.getAgentFocus(agent_id || "");
72777
74770
  const effectiveAgentId = focus ? focus.agent_id : agent_id || "";
72778
74771
  const effectiveProjectId = focus?.project_id || project_id;
74772
+ const cloud = getTodosCloudClient();
74773
+ if (cloud) {
74774
+ const remote = await cloudListTasks(cloud, {
74775
+ assigned_to: effectiveAgentId,
74776
+ status: status3,
74777
+ project_id: effectiveProjectId,
74778
+ limit: limit || 50
74779
+ });
74780
+ if (remote.length === 0)
74781
+ return { content: [{ type: "text", text: "No tasks found." }] };
74782
+ return { content: [{ type: "text", text: remote.map(formatTask).join(`
74783
+ `) }] };
74784
+ }
74785
+ const { listTasks: listTasks3 } = (init_tasks(), __toCommonJS(exports_tasks));
72779
74786
  const tasks = listTasks3({
72780
74787
  assigned_to: effectiveAgentId,
72781
74788
  status: status3,
@@ -72962,7 +74969,7 @@ function registerTaskMetaTools(server, ctx) {
72962
74969
  bulk_create_tasks: "bulk_create_tasks \u2014 Create multiple tasks. Params: tasks[]",
72963
74970
  bulk_delete_tasks: "bulk_delete_tasks \u2014 Delete multiple tasks. Params: task_ids[], force",
72964
74971
  archive_completed: "archive_completed \u2014 Auto-archive old completed tasks. Params: days, project_id",
72965
- get_archived_tasks: "get_archived_tasks \u2014 List archived tasks. Params: project_id, limit",
74972
+ get_archived_tasks: "get_archived_tasks \u2014 List archived tasks. Params: project_id, limit, offset",
72966
74973
  unarchive_task: "unarchive_task \u2014 Restore archived task. Params: task_id",
72967
74974
  auto_assign_task: "auto_assign_task \u2014 Auto-assign based on capabilities. Params: task_id",
72968
74975
  get_my_workload: "get_my_workload \u2014 Get agent workload stats. Params: agent_id",
@@ -72987,7 +74994,7 @@ function registerTaskMetaTools(server, ctx) {
72987
74994
  fail_task: "fail_task \u2014 Mark task failed. Params: task_id, reason, agent_id, version",
72988
74995
  register_agent: "register_agent \u2014 Register an agent. Params: name, description, role, title, capabilities, session_id, working_dir, force",
72989
74996
  list_agents: "list_agents \u2014 List registered agents. Params: include_archived",
72990
- get_agent: "get_agent \u2014 Get agent details. Params: agent_id, id, name",
74997
+ get_agent: "get_agent \u2014 Get agent details by exact ID. Params: agent_id (or id alias)",
72991
74998
  update_agent: "update_agent \u2014 Update an agent. Params: agent_id, id, name, description, role, title, level, capabilities, permissions, metadata",
72992
74999
  delete_agent: "delete_agent \u2014 Archive an agent. Params: agent_id, id, name",
72993
75000
  unarchive_agent: "unarchive_agent \u2014 Restore an archived agent. Params: agent_id, id, name",
@@ -72998,9 +75005,7 @@ function registerTaskMetaTools(server, ctx) {
72998
75005
  unfocus: "unfocus \u2014 Clear agent focus. Params: agent_id",
72999
75006
  suggest_agent_name: "suggest_agent_name \u2014 Suggest available agent names. Params: working_dir",
73000
75007
  get_org_chart: "get_org_chart \u2014 Get global org chart. Params: format",
73001
- set_reports_to: "set_reports_to \u2014 Set org hierarchy. Params: agent_id, reports_to",
73002
- extract_todos: "extract_todos \u2014 Scan code for TODO comments. Params: path, project_id, task_list_id, patterns, tags, assigned_to, agent_id, dry_run, extensions, exclude, respect_gitignore, include_index",
73003
- watch_source_todos: "watch_source_todos \u2014 Run finite local source TODO watcher scans. Params: path, project_id, task_list_id, patterns, tags, assigned_to, agent_id, dry_run, extensions, exclude, respect_gitignore, interval_ms, max_runs"
75008
+ set_reports_to: "set_reports_to \u2014 Set org hierarchy. Params: agent_id, reports_to"
73004
75009
  };
73005
75010
  if (toolDocs[tool_name]) {
73006
75011
  return { content: [{ type: "text", text: toolDocs[tool_name] }] };
@@ -80356,6 +82361,14 @@ ${lines.join(`
80356
82361
  committed_at: exports_external.string().optional().describe("ISO timestamp of commit")
80357
82362
  }, async ({ task_id, sha, message, author, files_changed, committed_at }) => {
80358
82363
  try {
82364
+ const cloud = getTodosCloudClient();
82365
+ if (cloud) {
82366
+ if (committed_at !== undefined) {
82367
+ throw new Error("HOSTED_FIELD_UNSUPPORTED: committed_at is not accepted by the current Todos /v1 commit contract; omit it rather than recording the wrong timestamp");
82368
+ }
82369
+ const linked = await cloudLinkCommit(cloud, task_id, { sha, message, author, files_changed });
82370
+ return { content: [{ type: "text", text: JSON.stringify(linked, null, 2) }] };
82371
+ }
80359
82372
  const resolvedId = resolveId(task_id);
80360
82373
  const commit = linkTaskToCommit({ task_id: resolvedId, sha, message, author, files_changed, committed_at });
80361
82374
  return { content: [{ type: "text", text: JSON.stringify(commit, null, 2) }] };
@@ -80367,6 +82380,14 @@ ${lines.join(`
80367
82380
  if (shouldRegisterTool("get_task_commits")) {
80368
82381
  server.tool("get_task_commits", "Get all git commits linked to a task.", { task_id: exports_external.string().describe("Task ID") }, async ({ task_id }) => {
80369
82382
  try {
82383
+ const cloud = getTodosCloudClient();
82384
+ if (cloud) {
82385
+ const task3 = await cloudGetTask(cloud, task_id);
82386
+ if (!task3)
82387
+ throw new Error(`Task not found: ${task_id}`);
82388
+ const remote = await cloudListTaskCommits(cloud, task3.id);
82389
+ return { content: [{ type: "text", text: JSON.stringify(remote, null, 2) }] };
82390
+ }
80370
82391
  const commits = getTaskCommits(resolveId(task_id));
80371
82392
  return { content: [{ type: "text", text: JSON.stringify(commits, null, 2) }] };
80372
82393
  } catch (e) {
@@ -80377,6 +82398,13 @@ ${lines.join(`
80377
82398
  if (shouldRegisterTool("find_task_by_commit")) {
80378
82399
  server.tool("find_task_by_commit", "Find which task a git commit SHA is linked to. Supports prefix matching.", { sha: exports_external.string().describe("Git commit SHA (full or short prefix)") }, async ({ sha }) => {
80379
82400
  try {
82401
+ const cloud = getTodosCloudClient();
82402
+ if (cloud) {
82403
+ const remote = await cloudFindCommit(cloud, sha);
82404
+ if (!remote)
82405
+ return { content: [{ type: "text", text: `No task linked to commit ${sha}` }] };
82406
+ return { content: [{ type: "text", text: JSON.stringify(remote, null, 2) }] };
82407
+ }
80380
82408
  const result = findTaskByCommit(sha);
80381
82409
  if (!result)
80382
82410
  return { content: [{ type: "text", text: `No task linked to commit ${sha}` }] };
@@ -80396,6 +82424,11 @@ ${lines.join(`
80396
82424
  metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata")
80397
82425
  }, async ({ task_id, ref_type, name, url, provider, metadata }) => {
80398
82426
  try {
82427
+ const cloud = getTodosCloudClient();
82428
+ if (cloud) {
82429
+ const linked = await cloudLinkRef(cloud, task_id, { ref_type, name, url, provider, metadata });
82430
+ return { content: [{ type: "text", text: JSON.stringify(linked, null, 2) }] };
82431
+ }
80399
82432
  const resolvedId = resolveId(task_id);
80400
82433
  const ref = linkTaskGitRef({ task_id: resolvedId, ref_type, name, url, provider, metadata });
80401
82434
  return { content: [{ type: "text", text: JSON.stringify(ref, null, 2) }] };
@@ -80407,6 +82440,11 @@ ${lines.join(`
80407
82440
  if (shouldRegisterTool("get_task_git_refs")) {
80408
82441
  server.tool("get_task_git_refs", "Get branches and pull requests linked to a task.", { task_id: exports_external.string().describe("Task ID") }, async ({ task_id }) => {
80409
82442
  try {
82443
+ const cloud = getTodosCloudClient();
82444
+ if (cloud) {
82445
+ const remote = await cloudListTaskRefs(cloud, task_id);
82446
+ return { content: [{ type: "text", text: JSON.stringify(remote, null, 2) }] };
82447
+ }
80410
82448
  const refs = getTaskGitRefs(resolveId(task_id));
80411
82449
  return { content: [{ type: "text", text: JSON.stringify(refs, null, 2) }] };
80412
82450
  } catch (e) {
@@ -80417,6 +82455,11 @@ ${lines.join(`
80417
82455
  if (shouldRegisterTool("find_tasks_by_git_ref")) {
80418
82456
  server.tool("find_tasks_by_git_ref", "Find tasks linked to a branch name, PR number, or PR URL.", { ref: exports_external.string().describe("Branch name, PR number, or PR URL substring") }, async ({ ref }) => {
80419
82457
  try {
82458
+ const cloud = getTodosCloudClient();
82459
+ if (cloud) {
82460
+ const remote = await cloudFindRefs(cloud, ref);
82461
+ return { content: [{ type: "text", text: JSON.stringify(remote, null, 2) }] };
82462
+ }
80420
82463
  const refs = findTasksByGitRef(ref);
80421
82464
  return { content: [{ type: "text", text: JSON.stringify(refs, null, 2) }] };
80422
82465
  } catch (e) {
@@ -80435,6 +82478,14 @@ ${lines.join(`
80435
82478
  run_at: exports_external.string().optional().describe("ISO timestamp when the command was run")
80436
82479
  }, async ({ task_id, command, status: status3, output_summary, artifact_path, agent_id, run_at }) => {
80437
82480
  try {
82481
+ const cloud = getTodosCloudClient();
82482
+ if (cloud) {
82483
+ if (run_at !== undefined) {
82484
+ throw new Error("HOSTED_FIELD_UNSUPPORTED: run_at is not accepted by the current Todos /v1 verification contract; omit it rather than recording the wrong timestamp");
82485
+ }
82486
+ const recorded = await cloudRecordVerification(cloud, task_id, { command, status: status3, output_summary, artifact_path, agent_id });
82487
+ return { content: [{ type: "text", text: JSON.stringify(recorded, null, 2) }] };
82488
+ }
80438
82489
  const verification2 = addTaskVerification({
80439
82490
  task_id: resolveId(task_id),
80440
82491
  command,
@@ -81148,6 +83199,7 @@ ${lines.join(`
81148
83199
  var init_task_resources = __esm(() => {
81149
83200
  init_zod();
81150
83201
  init_types();
83202
+ init_cloud_router();
81151
83203
  init_tasks();
81152
83204
  init_projects();
81153
83205
  init_agents();
@@ -83154,92 +85206,6 @@ var init_task_rel_tools = __esm(() => {
83154
85206
  init_zod();
83155
85207
  });
83156
85208
 
83157
- // src/mcp/tools/code-tools.ts
83158
- function registerCodeTools(server, ctx) {
83159
- const { shouldRegisterTool, resolveId, formatError, formatTask } = ctx;
83160
- if (shouldRegisterTool("extract_todos")) {
83161
- server.tool("extract_todos", "Scan source files for TODO/FIXME/HACK/BUG/XXX/NOTE comments and create tasks from them. Deduplicates on re-runs.", {
83162
- path: exports_external.string().describe("Directory or file path to scan"),
83163
- project_id: exports_external.string().optional().describe("Project to assign tasks to"),
83164
- task_list_id: exports_external.string().optional().describe("Task list to add tasks to"),
83165
- patterns: exports_external.array(exports_external.enum(["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"])).optional().describe("Tags to search for (default: all)"),
83166
- tags: exports_external.array(exports_external.string()).optional().describe("Extra tags to add to created tasks"),
83167
- assigned_to: exports_external.string().optional().describe("Agent to assign tasks to"),
83168
- agent_id: exports_external.string().optional().describe("Agent performing the extraction"),
83169
- dry_run: exports_external.boolean().optional().describe("If true, return found comments without creating tasks"),
83170
- extensions: exports_external.array(exports_external.string()).optional().describe("File extensions to scan (e.g. ['ts', 'py'])"),
83171
- exclude: exports_external.array(exports_external.string()).optional().describe("Gitignore-style path patterns to skip"),
83172
- respect_gitignore: exports_external.boolean().optional().describe("Respect .gitignore from the scanned root (default true)"),
83173
- include_index: exports_external.boolean().optional().describe("Include local codebase index metadata in the response")
83174
- }, async (params) => {
83175
- try {
83176
- const { extractTodos: extractTodos2 } = (init_extract(), __toCommonJS(exports_extract));
83177
- const resolved = { ...params };
83178
- if (resolved["project_id"])
83179
- resolved["project_id"] = resolveId(resolved["project_id"], "projects");
83180
- if (resolved["task_list_id"])
83181
- resolved["task_list_id"] = resolveId(resolved["task_list_id"], "task_lists");
83182
- const result = extractTodos2(resolved);
83183
- if (params.dry_run) {
83184
- if (params.include_index) {
83185
- return { content: [{ type: "text", text: JSON.stringify({ comments: result.comments, index: result.index }, null, 2) }] };
83186
- }
83187
- const lines = result.comments.map((c) => `[${c.tag}] ${c.message} \u2014 ${c.file}:${c.line}${c.symbol ? ` (${c.symbol})` : ""}`);
83188
- return { content: [{ type: "text", text: `Found ${result.comments.length} comment(s):
83189
- ${lines.join(`
83190
- `)}` }] };
83191
- }
83192
- const summary = [
83193
- `Created ${result.tasks.length} task(s)`,
83194
- result.skipped > 0 ? `Skipped ${result.skipped} duplicate(s)` : null,
83195
- `Total comments found: ${result.comments.length}`
83196
- ].filter(Boolean).join(`
83197
- `);
83198
- const taskLines = result.tasks.map(formatTask);
83199
- return { content: [{ type: "text", text: `${summary}
83200
-
83201
- ${taskLines.join(`
83202
- `)}` }] };
83203
- } catch (e) {
83204
- return { content: [{ type: "text", text: formatError(e) }], isError: true };
83205
- }
83206
- });
83207
- }
83208
- if (shouldRegisterTool("watch_source_todos")) {
83209
- server.tool("watch_source_todos", "Run finite local source TODO watcher scans. Uses polling, respects .gitignore/excludes, and can dry-run or apply extracted tasks.", {
83210
- path: exports_external.string().describe("Directory or file path to watch"),
83211
- project_id: exports_external.string().optional().describe("Project to assign tasks to"),
83212
- task_list_id: exports_external.string().optional().describe("Task list to add tasks to"),
83213
- patterns: exports_external.array(exports_external.enum(["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"])).optional().describe("Tags to search for (default: all)"),
83214
- tags: exports_external.array(exports_external.string()).optional().describe("Extra tags to add to created tasks"),
83215
- assigned_to: exports_external.string().optional().describe("Agent to assign tasks to"),
83216
- agent_id: exports_external.string().optional().describe("Agent performing the watcher scan"),
83217
- dry_run: exports_external.boolean().optional().describe("If true, return found comments without creating tasks"),
83218
- extensions: exports_external.array(exports_external.string()).optional().describe("File extensions to scan"),
83219
- exclude: exports_external.array(exports_external.string()).optional().describe("Gitignore-style path patterns to skip"),
83220
- respect_gitignore: exports_external.boolean().optional().describe("Respect .gitignore from the watched root"),
83221
- interval_ms: exports_external.number().optional().describe("Polling interval for repeated scans"),
83222
- max_runs: exports_external.number().optional().describe("Maximum scans before returning; default 1")
83223
- }, async (params) => {
83224
- try {
83225
- const { watchSourceTodos: watchSourceTodos2 } = (init_extract(), __toCommonJS(exports_extract));
83226
- const resolved = { ...params, include_index: true, once: (params.max_runs || 1) <= 1 };
83227
- if (resolved["project_id"])
83228
- resolved["project_id"] = resolveId(resolved["project_id"], "projects");
83229
- if (resolved["task_list_id"])
83230
- resolved["task_list_id"] = resolveId(resolved["task_list_id"], "task_lists");
83231
- const result = await watchSourceTodos2(resolved);
83232
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
83233
- } catch (e) {
83234
- return { content: [{ type: "text", text: formatError(e) }], isError: true };
83235
- }
83236
- });
83237
- }
83238
- }
83239
- var init_code_tools = __esm(() => {
83240
- init_zod();
83241
- });
83242
-
83243
85209
  // src/cli/machine-api.ts
83244
85210
  import { resolve as resolve22 } from "path";
83245
85211
  function receipt(raw) {
@@ -83702,9 +85668,11 @@ Last seen: ${agent.last_seen_at}`
83702
85668
  try {
83703
85669
  const pool = getAgentPoolForProject(working_dir);
83704
85670
  const cutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
83705
- const allActive = listAgents().filter((a) => a.last_seen_at > cutoff);
85671
+ const cloud = getTodosCloudClient();
85672
+ const roster = cloud ? await cloudListAgents(cloud) : listAgents();
85673
+ const allActive = roster.filter((a) => (a.last_seen_at ?? "") > cutoff);
83706
85674
  if (!pool) {
83707
- const suggestions = getAvailableNamesFromPool([
85675
+ const defaultPool = [
83708
85676
  "caesar",
83709
85677
  "augustus",
83710
85678
  "marcus",
@@ -83724,7 +85692,8 @@ Last seen: ${agent.last_seen_at}`
83724
85692
  "thalia",
83725
85693
  "phoebe",
83726
85694
  "daphne"
83727
- ], getDatabase());
85695
+ ];
85696
+ const suggestions = cloud ? defaultPool.filter((name) => !roster.some((a) => a.name?.toLowerCase() === name)) : getAvailableNamesFromPool(defaultPool, getDatabase());
83728
85697
  const lines2 = [
83729
85698
  "No project pool configured. Use a distinctive one-word name; generic generated names are blocked.",
83730
85699
  `Suggested names: ${suggestions.slice(0, 8).join(", ")}`,
@@ -83735,8 +85704,8 @@ To restrict names, configure agent_pool or project_pools in the todos data home
83735
85704
  return { content: [{ type: "text", text: lines2.join(`
83736
85705
  `) }] };
83737
85706
  }
83738
- const available = getAvailableNamesFromPool(pool, getDatabase());
83739
- const activeInPool = allActive.filter((a) => pool.map((n) => n.toLowerCase()).includes(a.name));
85707
+ const available = cloud ? pool.filter((name) => !roster.some((a) => a.name?.toLowerCase() === name.toLowerCase())) : getAvailableNamesFromPool(pool, getDatabase());
85708
+ const activeInPool = allActive.filter((agent) => pool.map((name) => name.toLowerCase()).includes(agent.name.toLowerCase()));
83740
85709
  const lines = [
83741
85710
  `Project pool: ${pool.join(", ")}`,
83742
85711
  `Available now (${available.length}): ${available.length > 0 ? available.join(", ") : "none \u2014 all names in use"}`,
@@ -83758,7 +85727,7 @@ No names available. Wait for an active agent to go stale (30min timeout).`
83758
85727
  }, async ({ include_archived }) => {
83759
85728
  try {
83760
85729
  const cloud = getTodosCloudClient();
83761
- const agents = cloud ? await cloudListAgents(cloud) : listAgents({ include_archived: include_archived ?? false });
85730
+ const agents = cloud ? await cloudListAgents(cloud, { include_archived: include_archived ?? false }) : listAgents({ include_archived: include_archived ?? false });
83762
85731
  if (agents.length === 0) {
83763
85732
  return { content: [{ type: "text", text: "No agents registered." }] };
83764
85733
  }
@@ -83775,19 +85744,19 @@ ${text2}` }] };
83775
85744
  });
83776
85745
  }
83777
85746
  if (shouldRegisterTool("get_agent")) {
83778
- server.tool("get_agent", "Get agent details by ID or name. Provide one of id or name.", {
83779
- agent_id: exports_external.string().optional(),
83780
- id: exports_external.string().optional(),
83781
- name: exports_external.string().optional()
83782
- }, async ({ agent_id, id, name }) => {
85747
+ server.tool("get_agent", "Get agent details by exact immutable ID. Use list_agents to resolve a name first.", {
85748
+ agent_id: exports_external.string().optional().describe("Exact immutable agent ID"),
85749
+ id: exports_external.string().optional().describe("Compatibility alias for agent_id; exact immutable agent ID")
85750
+ }, async ({ agent_id, id }) => {
83783
85751
  try {
83784
- const identifier2 = agent_id || id || name;
85752
+ const identifier2 = (agent_id || id || "").trim();
83785
85753
  if (!identifier2) {
83786
- return { content: [{ type: "text", text: "Provide agent_id, id, or name." }], isError: true };
85754
+ return { content: [{ type: "text", text: "Provide the exact agent_id. Use list_agents to resolve a name first." }], isError: true };
83787
85755
  }
83788
- const agent = getAgent(identifier2) || getAgentByName(identifier2);
85756
+ const cloud = getTodosCloudClient();
85757
+ const agent = cloud ? await cloudGetAgent(cloud, identifier2) : getAgent(identifier2);
83789
85758
  if (!agent) {
83790
- return { content: [{ type: "text", text: `Agent not found: ${identifier2}` }], isError: true };
85759
+ return { content: [{ type: "text", text: `Agent not found by exact ID: ${identifier2}` }], isError: true };
83791
85760
  }
83792
85761
  const parts = [
83793
85762
  `ID: ${agent.id}`,
@@ -83795,7 +85764,7 @@ ${text2}` }] };
83795
85764
  ];
83796
85765
  if (agent.description)
83797
85766
  parts.push(`Description: ${agent.description}`);
83798
- if (Object.keys(agent.metadata).length > 0)
85767
+ if (agent.metadata && Object.keys(agent.metadata).length > 0)
83799
85768
  parts.push(`Metadata: ${JSON.stringify(agent.metadata)}`);
83800
85769
  parts.push(`Created: ${agent.created_at}`);
83801
85770
  parts.push(`Last seen: ${agent.last_seen_at}`);
@@ -84083,6 +86052,13 @@ function registerTemplateTools(server, { shouldRegisterTool, resolveId, formatEr
84083
86052
  })).optional().describe("Multi-task template: ordered list of tasks to create together with dependencies")
84084
86053
  }, async (params) => {
84085
86054
  try {
86055
+ const cloud = getTodosCloudClient();
86056
+ if (cloud) {
86057
+ const created = await cloudCreateTemplate(cloud, params);
86058
+ const remoteCount = created.tasks?.length ?? 0;
86059
+ const remoteInfo = remoteCount > 0 ? ` | ${remoteCount} task(s)` : "";
86060
+ return { content: [{ type: "text", text: `Template created: ${created.id.slice(0, 8)} | ${created.name} | "${created.title_pattern}"${remoteInfo}` }] };
86061
+ }
84086
86062
  const { createTemplate: createTemplate3, getTemplateWithTasks: getTemplateWithTasks2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
84087
86063
  const t = createTemplate3(params);
84088
86064
  const withTasks = getTemplateWithTasks2(t.id);
@@ -84097,8 +86073,8 @@ function registerTemplateTools(server, { shouldRegisterTool, resolveId, formatEr
84097
86073
  if (shouldRegisterTool("list_templates")) {
84098
86074
  server.tool("list_templates", "List all task templates", {}, async () => {
84099
86075
  try {
84100
- const { listTemplates: listTemplates2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
84101
- const templates = listTemplates2();
86076
+ const cloud = getTodosCloudClient();
86077
+ const templates = cloud ? await listSharedTemplates(cloud) : (await Promise.resolve().then(() => (init_templates(), exports_templates))).listTemplates();
84102
86078
  if (templates.length === 0)
84103
86079
  return { content: [{ type: "text", text: "No templates." }] };
84104
86080
  const text2 = templates.map((t) => {
@@ -84125,6 +86101,28 @@ ${text2}` }] };
84125
86101
  variables: exports_external.record(exports_external.string()).optional().describe("Variable substitution map for {name} placeholders in multi-task templates")
84126
86102
  }, async (params) => {
84127
86103
  try {
86104
+ const cloud = getTodosCloudClient();
86105
+ if (cloud) {
86106
+ const template = await resolveRemoteTemplate(cloud, params.template_id);
86107
+ if (!template)
86108
+ return { content: [{ type: "text", text: `Template not found: ${params.template_id}` }], isError: true };
86109
+ const projectRef = params.project_id ? await cloudResolveProjectRef(cloud, params.project_id) : undefined;
86110
+ const effectiveProject = projectRef ?? template.project_id ?? undefined;
86111
+ const taskListId = params.task_list_id ? await cloudResolveTaskListRef(cloud, params.task_list_id, effectiveProject) : undefined;
86112
+ const { tasks } = await createRemoteTemplateTasks(cloud, template, effectiveProject, params.variables ?? {}, {
86113
+ assignedTo: params.assigned_to,
86114
+ taskListId,
86115
+ overrides: {
86116
+ title: params.title,
86117
+ description: params.description,
86118
+ priority: params.priority
86119
+ }
86120
+ });
86121
+ const remoteText = tasks.map((t) => `${t.id.slice(0, 8)} | ${t.priority} | ${t.title}`).join(`
86122
+ `);
86123
+ return { content: [{ type: "text", text: `${tasks.length} task(s) created from template:
86124
+ ${remoteText}` }] };
86125
+ }
84128
86126
  const { taskFromTemplate: taskFromTemplate2, getTemplateWithTasks: getTemplateWithTasks2, tasksFromTemplate: tasksFromTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
84129
86127
  const resolvedTemplateId = resolveId(params.template_id, "task_templates");
84130
86128
  const resolvedProjectId = params.project_id ? resolveId(params.project_id, "projects") : undefined;
@@ -84157,6 +86155,12 @@ ${task3.id.slice(0, 8)} | ${task3.priority} | ${task3.title}` }] };
84157
86155
  if (shouldRegisterTool("delete_template")) {
84158
86156
  server.tool("delete_template", "Delete a task template by ID.", { id: exports_external.string() }, async ({ id }) => {
84159
86157
  try {
86158
+ const cloud = getTodosCloudClient();
86159
+ if (cloud) {
86160
+ const template = await resolveRemoteTemplate(cloud, id);
86161
+ const removed = template ? await cloudDeleteTemplate(cloud, template.id) : false;
86162
+ return { content: [{ type: "text", text: removed ? "Template deleted." : "Template not found." }] };
86163
+ }
84160
86164
  const { deleteTemplate: deleteTemplate3 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
84161
86165
  const resolvedId = resolveId(id, "task_templates");
84162
86166
  const deleted = deleteTemplate3(resolvedId);
@@ -84178,6 +86182,14 @@ ${task3.id.slice(0, 8)} | ${task3.priority} | ${task3.title}` }] };
84178
86182
  plan_id: exports_external.string().optional()
84179
86183
  }, async ({ id, ...updates }) => {
84180
86184
  try {
86185
+ const cloud = getTodosCloudClient();
86186
+ if (cloud) {
86187
+ const prior = await resolveRemoteTemplate(cloud, id);
86188
+ if (!prior)
86189
+ return { content: [{ type: "text", text: `Template not found: ${id}` }], isError: true };
86190
+ const patched = await updateSharedTemplate(cloud, prior.id, { ...updates, expected_version: prior.version });
86191
+ return { content: [{ type: "text", text: `Template updated: ${patched.id.slice(0, 8)} | ${patched.name} | "${patched.title_pattern}" | ${patched.priority}` }] };
86192
+ }
84181
86193
  const { updateTemplate: updateTemplate3 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
84182
86194
  const resolvedId = resolveId(id, "task_templates");
84183
86195
  const t = updateTemplate3(resolvedId, updates);
@@ -84192,8 +86204,8 @@ ${task3.id.slice(0, 8)} | ${task3.priority} | ${task3.title}` }] };
84192
86204
  if (shouldRegisterTool("init_templates")) {
84193
86205
  server.tool("init_templates", "Initialize the bundled local template library. Skips templates that already exist by name.", {}, async () => {
84194
86206
  try {
84195
- const { initBuiltinTemplates: initBuiltinTemplates2 } = await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates));
84196
- const result = initBuiltinTemplates2();
86207
+ const cloud = getTodosCloudClient();
86208
+ const result = cloud ? await initializeSharedTemplates(cloud) : (await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates))).initBuiltinTemplates();
84197
86209
  if (result.created === 0)
84198
86210
  return { content: [{ type: "text", text: `All ${result.skipped} built-in template(s) already exist.` }] };
84199
86211
  return { content: [{ type: "text", text: `Created ${result.created} template(s): ${result.names.join(", ")}. Skipped ${result.skipped} existing.` }] };
@@ -84205,8 +86217,7 @@ ${task3.id.slice(0, 8)} | ${task3.priority} | ${task3.title}` }] };
84205
86217
  if (shouldRegisterTool("list_template_library")) {
84206
86218
  server.tool("list_template_library", "List the bundled marketplace-free local template library without mutating the database.", {}, async () => {
84207
86219
  try {
84208
- const { listBuiltinTemplates: listBuiltinTemplates2 } = await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates));
84209
- const templates = listBuiltinTemplates2().map((template) => ({
86220
+ const templates = listBuiltinTemplates().map((template) => ({
84210
86221
  name: template.name,
84211
86222
  description: template.description,
84212
86223
  category: template.category,
@@ -84223,8 +86234,7 @@ ${task3.id.slice(0, 8)} | ${task3.priority} | ${task3.title}` }] };
84223
86234
  if (shouldRegisterTool("write_template_library")) {
84224
86235
  server.tool("write_template_library", "Write the bundled local template library to editable JSON files for review or import.", { directory: exports_external.string() }, async ({ directory }) => {
84225
86236
  try {
84226
- const { writeBuiltinTemplateFiles: writeBuiltinTemplateFiles2 } = await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates));
84227
- return { content: [{ type: "text", text: JSON.stringify(writeBuiltinTemplateFiles2(directory), null, 2) }] };
86237
+ return { content: [{ type: "text", text: JSON.stringify(writeBuiltinTemplateFiles(directory), null, 2) }] };
84228
86238
  } catch (e) {
84229
86239
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
84230
86240
  }
@@ -84236,9 +86246,17 @@ ${task3.id.slice(0, 8)} | ${task3.priority} | ${task3.title}` }] };
84236
86246
  variables: exports_external.record(exports_external.string()).optional().describe("Variable substitution map for {name} placeholders")
84237
86247
  }, async (params) => {
84238
86248
  try {
84239
- const { previewTemplate: previewTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
84240
- const resolvedId = resolveId(params.template_id, "task_templates");
84241
- const preview = previewTemplate2(resolvedId, params.variables);
86249
+ const cloud = getTodosCloudClient();
86250
+ let preview;
86251
+ if (cloud) {
86252
+ const template = await resolveRemoteTemplate(cloud, params.template_id);
86253
+ if (!template)
86254
+ return { content: [{ type: "text", text: `Template not found: ${params.template_id}` }], isError: true };
86255
+ preview = previewRemoteTemplate(template, params.variables);
86256
+ } else {
86257
+ const { previewTemplate: previewTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
86258
+ preview = previewTemplate2(resolveId(params.template_id, "task_templates"), params.variables);
86259
+ }
84242
86260
  const lines = preview.tasks.map((t) => {
84243
86261
  const deps = t.depends_on_positions.length > 0 ? ` (after: ${t.depends_on_positions.join(", ")})` : "";
84244
86262
  return ` [${t.position}] ${t.priority} | ${t.title}${deps}`;
@@ -84258,6 +86276,13 @@ ${lines.join(`
84258
86276
  if (shouldRegisterTool("export_template")) {
84259
86277
  server.tool("export_template", "Export a template as a full JSON object (template + tasks + variables). Useful for sharing or backup.", { template_id: exports_external.string() }, async ({ template_id }) => {
84260
86278
  try {
86279
+ const cloud = getTodosCloudClient();
86280
+ if (cloud) {
86281
+ const template = await resolveRemoteTemplate(cloud, template_id);
86282
+ if (!template)
86283
+ return { content: [{ type: "text", text: `Template not found: ${template_id}` }], isError: true };
86284
+ return { content: [{ type: "text", text: JSON.stringify(exportRemoteTemplate(template), null, 2) }] };
86285
+ }
84261
86286
  const { exportTemplate: exportTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
84262
86287
  const resolvedId = resolveId(template_id, "task_templates");
84263
86288
  const json7 = exportTemplate2(resolvedId);
@@ -84270,8 +86295,13 @@ ${lines.join(`
84270
86295
  if (shouldRegisterTool("import_template")) {
84271
86296
  server.tool("import_template", "Import a template from a JSON string (as returned by export_template). Creates new template with new IDs.", { json: exports_external.string().describe("JSON string of the template export") }, async ({ json: json7 }) => {
84272
86297
  try {
84273
- const { importTemplate: importTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
84274
86298
  const parsed = JSON.parse(json7);
86299
+ const cloud = getTodosCloudClient();
86300
+ if (cloud) {
86301
+ const imported = await cloudCreateTemplate(cloud, parsed);
86302
+ return { content: [{ type: "text", text: `Template imported: ${imported.id.slice(0, 8)} | ${imported.name} | "${imported.title_pattern}"` }] };
86303
+ }
86304
+ const { importTemplate: importTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
84275
86305
  const t = importTemplate2(parsed);
84276
86306
  return { content: [{ type: "text", text: `Template imported: ${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}"` }] };
84277
86307
  } catch (e) {
@@ -84282,6 +86312,22 @@ ${lines.join(`
84282
86312
  if (shouldRegisterTool("template_history")) {
84283
86313
  server.tool("template_history", "Show version history of a template. Each update creates a snapshot of the previous state.", { template_id: exports_external.string() }, async ({ template_id }) => {
84284
86314
  try {
86315
+ const cloud = getTodosCloudClient();
86316
+ if (cloud) {
86317
+ const template2 = await resolveRemoteTemplate(cloud, template_id);
86318
+ if (!template2)
86319
+ return { content: [{ type: "text", text: `Template not found: ${template_id}` }], isError: true };
86320
+ const history = await readSharedTemplateHistory(cloud, template2.id);
86321
+ if (history.versions.length === 0)
86322
+ return { content: [{ type: "text", text: `${template2.name} v${template2.version} \u2014 no previous versions.` }] };
86323
+ const remoteLines = history.versions.map((v) => {
86324
+ const snap = typeof v.snapshot === "string" ? JSON.parse(v.snapshot) : v.snapshot;
86325
+ return `v${v.version} | ${v.created_at} | ${snap.name} | "${snap.title_pattern}"`;
86326
+ });
86327
+ return { content: [{ type: "text", text: `${template2.name} \u2014 current: v${template2.version}
86328
+ ${remoteLines.join(`
86329
+ `)}` }] };
86330
+ }
84285
86331
  const { listTemplateVersions: listTemplateVersions2, getTemplate: getTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
84286
86332
  const resolvedId = resolveId(template_id, "task_templates");
84287
86333
  const template = getTemplate2(resolvedId);
@@ -84306,6 +86352,9 @@ ${lines.join(`
84306
86352
  var init_templates2 = __esm(() => {
84307
86353
  init_zod();
84308
86354
  init_tasks();
86355
+ init_cloud_router();
86356
+ init_template_remote();
86357
+ init_builtin_template_library();
84309
86358
  });
84310
86359
 
84311
86360
  // src/lib/environment-snapshots.ts
@@ -84670,11 +86719,16 @@ var init_workflow_prompts2 = __esm(() => {
84670
86719
  // src/mcp/index.ts
84671
86720
  var exports_mcp = {};
84672
86721
  __export(exports_mcp, {
86722
+ resolveTodosMcpAuthority: () => resolveTodosMcpAuthority,
86723
+ mcpHttpRefusal: () => mcpHttpRefusal,
86724
+ hostedRouteLocalStoreRefusal: () => hostedRouteLocalStoreRefusal,
84673
86725
  formatTaskDetail: () => formatTaskDetail,
84674
86726
  formatTask: () => formatTask,
84675
86727
  formatError: () => formatError,
84676
86728
  buildServer: () => buildServer,
84677
- applyFocus: () => applyFocus
86729
+ applyFocus: () => applyFocus,
86730
+ agentFocusMap: () => agentFocusMap,
86731
+ MCP_HTTP_REFUSED_EXIT_CODE: () => MCP_HTTP_REFUSED_EXIT_CODE
84678
86732
  });
84679
86733
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
84680
86734
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -84693,18 +86747,18 @@ function printHelp() {
84693
86747
  Start the @hasna/todos MCP server.
84694
86748
 
84695
86749
  Options:
84696
- --stdio Use stdio transport (default)
84697
- --http Use Streamable HTTP transport
84698
- --port <port> Use Streamable HTTP on the given port (implies --http)
86750
+ --stdio Use stdio transport (the only transport this binary serves)
84699
86751
  -V, --version output the version number
84700
86752
  -h, --help display help for command
84701
86753
 
84702
86754
  Environment:
84703
86755
  MCP_STDIO=1 Force stdio transport
84704
- MCP_HTTP=1 Use Streamable HTTP transport
84705
- MCP_HTTP_PORT=<port> HTTP port when using HTTP transport
84706
86756
  TODOS_PROFILE=<profile> Tool profile filter
84707
- TODOS_TOOL_GROUPS=<list> Comma-separated tool group filter`);
86757
+ TODOS_TOOL_GROUPS=<list> Comma-separated tool group filter
86758
+
86759
+ The Streamable HTTP transport (POST /mcp) is served by \`todos-serve\`, which owns
86760
+ the HTTP listener and its auth posture; \`--http\`, \`--port\` and MCP_HTTP=1 are
86761
+ refused here (exit 2).`);
84708
86762
  }
84709
86763
  function shouldRegisterTool(name) {
84710
86764
  return shouldRegisterToolForProfile2(name);
@@ -84713,7 +86767,7 @@ function getAgentFocus(agentId) {
84713
86767
  const sessionFocus = agentFocusMap.get(agentId);
84714
86768
  if (sessionFocus)
84715
86769
  return sessionFocus;
84716
- if (getTodosCloudClient())
86770
+ if (isLocalStoreRefused() || hasTodosEnvAuthorityIntent(process.env) || getTodosCloudClient())
84717
86771
  return;
84718
86772
  try {
84719
86773
  const agent = getAgentByName(agentId) || getAgent(agentId);
@@ -84734,11 +86788,14 @@ function applyFocus(params, agentId) {
84734
86788
  }
84735
86789
  }
84736
86790
  function guardRefusalPayload(message) {
84737
- const match = /^(REMOTE_API_[A-Z_]+|API_DATABASE_FALLBACK_FORBIDDEN):\s*([\s\S]*)$/.exec(message);
86791
+ const match = /^(REMOTE_API_[A-Z_]+|REMOTE_COMMAND_[A-Z_]+|API_DATABASE_FALLBACK_FORBIDDEN):\s*([\s\S]*)$/.exec(message);
84738
86792
  if (!match)
84739
86793
  return;
84740
86794
  const code = match[1];
84741
86795
  const detail = match[2].trim();
86796
+ if (code.startsWith("REMOTE_COMMAND_")) {
86797
+ return JSON.stringify({ code, message: detail || message });
86798
+ }
84742
86799
  return JSON.stringify({
84743
86800
  code,
84744
86801
  message: detail || message,
@@ -84934,7 +86991,6 @@ function buildServer() {
84934
86991
  registerTaskMetaTools(server, toolContext);
84935
86992
  registerTaskResources(server, toolContext);
84936
86993
  registerTaskRelTools(server, toolContext);
84937
- registerCodeTools(server, toolContext);
84938
86994
  registerAgentTools(server, { ...toolContext, agentFocusMap });
84939
86995
  registerTemplateTools(server, toolContext);
84940
86996
  registerEnvironmentSnapshotTools(server, toolContext);
@@ -84943,30 +86999,57 @@ function buildServer() {
84943
86999
  registerDispatchTools(server, { shouldRegisterTool: shouldRegisterToolOnce, resolveId, formatError });
84944
87000
  return server;
84945
87001
  }
87002
+ function resolveTodosMcpAuthority(env2 = process.env) {
87003
+ const resolution = resolveTodosCliTransport(env2);
87004
+ if (resolution.transport === "sqlite") {
87005
+ return { route: "local", notice: todosLocalModeNotice("local-opt-in") };
87006
+ }
87007
+ return { route: "hosted", v1_base_url: resolution.authority.baseUrl, source: resolution.source };
87008
+ }
87009
+ function hostedRouteLocalStoreRefusal(v1BaseUrl) {
87010
+ return "REMOTE_COMMAND_UNSUPPORTED: this tool or resource is local-only \u2014 it reads the on-box SQLite " + `store, which the hosted Todos /v1 authority (${v1BaseUrl}) does not serve; local SQLite is ` + "opt-in only (HASNA_TODOS_LOCAL=1, alias TODOS_LOCAL=1) and is disabled by default. Use the hosted " + "tools on this route (list_tasks, list_projects, list_agents, get_status, ...) instead.";
87011
+ }
87012
+ function mcpHttpRefusal() {
87013
+ return "todos-mcp: the HTTP transport is not served by this binary (stdio only). " + "Use `todos-serve` \u2014 it mounts the MCP Streamable HTTP endpoint at POST /mcp with its own auth " + 'posture (see README "REST API"). `--http`, `--port` and MCP_HTTP=1 are refused.';
87014
+ }
84946
87015
  async function main() {
84947
- const { isHttpMode, resolveHttpPort } = await Promise.resolve().then(() => (init_http4(), exports_http));
87016
+ const { isHttpMode } = await Promise.resolve().then(() => (init_http4(), exports_http));
84948
87017
  const portRequested = process.argv.some((arg) => arg === "--port" || arg.startsWith("--port="));
84949
- if (!isHttpMode() && !portRequested) {
84950
- const server = buildServer();
87018
+ if (isHttpMode() || portRequested) {
87019
+ process.stderr.write(`${mcpHttpRefusal()}
87020
+ `);
87021
+ process.exit(MCP_HTTP_REFUSED_EXIT_CODE);
87022
+ }
87023
+ let authority;
87024
+ try {
87025
+ authority = resolveTodosMcpAuthority();
87026
+ } catch (error2) {
87027
+ process.stderr.write(`${error2 instanceof Error ? error2.message : String(error2)}
87028
+ `);
87029
+ process.exit(1);
87030
+ }
87031
+ if (authority.route === "hosted") {
87032
+ refuseLocalStore(hostedRouteLocalStoreRefusal(authority.v1_base_url));
87033
+ }
87034
+ const server = buildServer();
87035
+ if (authority.route === "local") {
87036
+ process.stderr.write(`${authority.notice}
87037
+ `);
84951
87038
  try {
84952
87039
  const { startRuntimeShadowDrain: startRuntimeShadowDrain2 } = await Promise.resolve().then(() => (init_shadow_runtime(), exports_shadow_runtime));
84953
- if (!getTodosCloudClient())
84954
- startRuntimeShadowDrain2(getDatabase());
87040
+ startRuntimeShadowDrain2(getDatabase());
84955
87041
  } catch {}
84956
- const transport = new StdioServerTransport;
84957
- await server.connect(transport);
84958
- return;
84959
87042
  }
84960
- const { startServer } = await Promise.resolve().then(() => (init_serve(), exports_serve));
84961
- const port = resolveHttpPort();
84962
- await startServer(port, { host: "127.0.0.1", allowAnonymous: true });
84963
- console.error(`todos MCP HTTP mounted at http://127.0.0.1:${port}/mcp`);
87043
+ const transport = new StdioServerTransport;
87044
+ await server.connect(transport);
84964
87045
  }
84965
- var agentFocusMap, API_DATABASE_FALLBACK_FORBIDDEN_SUGGESTION, isDirectRun;
87046
+ var agentFocusMap, API_DATABASE_FALLBACK_FORBIDDEN_SUGGESTION, MCP_HTTP_REFUSED_EXIT_CODE = 2, isDirectRun;
84966
87047
  var init_mcp2 = __esm(() => {
84967
87048
  init_cloud_router();
84968
87049
  init_agents();
84969
87050
  init_database();
87051
+ init_cloud_router();
87052
+ init_local_opt_in();
84970
87053
  init_lock_display();
84971
87054
  init_types();
84972
87055
  init_local_encryption();
@@ -84979,7 +87062,6 @@ var init_mcp2 = __esm(() => {
84979
87062
  init_task_meta_tools();
84980
87063
  init_task_resources();
84981
87064
  init_task_rel_tools();
84982
- init_code_tools();
84983
87065
  init_machines2();
84984
87066
  init_agents2();
84985
87067
  init_templates2();
@@ -85387,7 +87469,7 @@ function resolveClientIp(req, server) {
85387
87469
  trustedProxies: trustedProxiesFromEnv()
85388
87470
  }) ?? "unknown";
85389
87471
  }
85390
- function checkRateLimit(ip) {
87472
+ function checkRateLimit(ip, maximum) {
85391
87473
  const now4 = Date.now();
85392
87474
  const entry2 = rateLimitMap.get(ip);
85393
87475
  if (!entry2 || now4 > entry2.resetAt) {
@@ -85395,7 +87477,7 @@ function checkRateLimit(ip) {
85395
87477
  return { allowed: true };
85396
87478
  }
85397
87479
  entry2.count++;
85398
- if (entry2.count > RATE_LIMIT_MAX) {
87480
+ if (entry2.count > maximum) {
85399
87481
  return { allowed: false, retryAfter: Math.ceil((entry2.resetAt - now4) / 1000) };
85400
87482
  }
85401
87483
  return { allowed: true };
@@ -85449,6 +87531,7 @@ function hasGeneratedApiKeysSafely() {
85449
87531
  }
85450
87532
  }
85451
87533
  async function startServer(port, options) {
87534
+ const rateLimitMax = resolveRateLimitMax(env.rateLimitMax());
85452
87535
  const envServerKey = resolveServerKeyEnv();
85453
87536
  const cliApiKey = options?.apiKey || null;
85454
87537
  const apiKey = cliApiKey || envServerKey?.value || null;
@@ -85543,7 +87626,7 @@ data: ${data}
85543
87626
  }
85544
87627
  const ip = resolveClientIp(req, server2);
85545
87628
  const peerIp = server2.requestIP(req)?.address;
85546
- const rl = checkRateLimit(ip);
87629
+ const rl = checkRateLimit(ip, rateLimitMax);
85547
87630
  if (!rl.allowed) {
85548
87631
  return new Response(JSON.stringify({ error: "Too many requests", retry_after: rl.retryAfter }), {
85549
87632
  status: 429,
@@ -85818,7 +87901,7 @@ data: ${data}
85818
87901
  const serverUrl = `http://localhost:${boundPort}`;
85819
87902
  console.log(`Todos HTTP server running at ${serverUrl}`);
85820
87903
  }
85821
- var SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX;
87904
+ var SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000;
85822
87905
  var init_serve = __esm(() => {
85823
87906
  init_database();
85824
87907
  init_api_keys();
@@ -85834,7 +87917,6 @@ var init_serve = __esm(() => {
85834
87917
  "Permissions-Policy": "camera=, microphone=, geolocation="
85835
87918
  };
85836
87919
  rateLimitMap = new Map;
85837
- RATE_LIMIT_MAX = Number.parseInt(env.rateLimitMax() || "120", 10);
85838
87920
  });
85839
87921
 
85840
87922
  // src/cli/components/Header.tsx
@@ -88367,7 +90449,7 @@ ${tasks.length} task(s) shown`));
88367
90449
  });
88368
90450
  await new Promise(() => {});
88369
90451
  });
88370
- program2.command("stream").description("Subscribe to real-time task events via SSE (requires todos serve)").option("--agent <id>", "Filter to events for a specific agent").option("--events <list>", "Comma-separated event types (default: all)", "task.created,task.started,task.completed,task.failed,task.assigned,task.status_changed").option("--port <n>", "Server port", "3000").option("--json", "Output raw JSON events").action(async (opts) => {
90452
+ program2.command("stream").description("Subscribe to real-time task events via SSE (requires todos serve)").option("--agent <id>", "Filter to events for a specific agent").option("--events <list>", "Comma-separated event types (default: all)", "task.created,task.started,task.completed,task.failed,task.assigned,task.status_changed").option("--port <n>", "Port of the local `todos serve` to subscribe to", String(DEFAULT_PORT)).option("--json", "Output raw JSON events").action(async (opts) => {
88371
90453
  const baseUrl = `http://localhost:${opts.port}`;
88372
90454
  const params = new URLSearchParams;
88373
90455
  if (opts.agent)
@@ -97185,17 +99267,23 @@ var init_cli_mcp_parity = __esm(() => {
97185
99267
  "todos extract",
97186
99268
  "todos extract-watch"
97187
99269
  ],
97188
- mcpTools: [
97189
- "extract_todos",
97190
- "watch_source_todos"
97191
- ],
99270
+ mcpTools: [],
97192
99271
  jsonContracts: ["source_code_index", "source_todo_comment", "task", "structured_error", "api_error"],
97193
99272
  errorContracts: ["structured_error", "api_error"],
97194
- status: "matched",
97195
- intentionalGaps: [],
99273
+ status: "intentional-gap",
99274
+ intentionalGaps: [
99275
+ {
99276
+ cliCommand: "todos extract",
99277
+ reason: "The source TODO index scans the local checkout the CLI runs in; the former extract_todos MCP tool read the agent host's filesystem behind a task-store tool surface and had no hosted arm, so it was removed (fleet alignment 2026-09-11)."
99278
+ },
99279
+ {
99280
+ cliCommand: "todos extract-watch",
99281
+ reason: "A polling filesystem watcher is a long-running local process, not an MCP tool call; the former watch_source_todos tool was removed with extract_todos \u2014 run `todos extract-watch` from the checkout instead."
99282
+ }
99283
+ ],
99284
+ gapReason: "Source scanning and watching are local-filesystem CLI operations; no MCP tool mapping.",
97196
99285
  example: {
97197
- cli: "todos extract . --dry-run --index --json",
97198
- mcpTool: "extract_todos"
99286
+ cli: "todos extract . --dry-run --index --json"
97199
99287
  }
97200
99288
  },
97201
99289
  {
@@ -102288,7 +104376,7 @@ function registerHelpCommands(program2, route = "local", remoteCapabilities = ne
102288
104376
  });
102289
104377
  const format = opts.json || globalOpts.json ? "json" : opts.format || "markdown";
102290
104378
  if (format === "json") {
102291
- console.log(JSON.stringify(manual));
104379
+ printJson(manual);
102292
104380
  return;
102293
104381
  }
102294
104382
  if (format !== "markdown")