@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/index.js CHANGED
@@ -4117,20 +4117,17 @@ function ensureAgentIdentitySchema(db) {
4117
4117
  SELECT RAISE(ABORT, 'IDENTITY_SOURCE_LINEAGE_IMMUTABLE');
4118
4118
  END;
4119
4119
 
4120
+ -- Drop first so an older definition is replaced rather than silently
4121
+ -- retained (the upgrade path). AGENT_IDENTITY_HISTORY_TRIGGER_DDL then
4122
+ -- re-creates them with CREATE TRIGGER IF NOT EXISTS. The two statements
4123
+ -- do different jobs and both are kept: the DROP is what refreshes a
4124
+ -- stale definition, and the IF NOT EXISTS is what keeps the CREATE safe
4125
+ -- on its own \u2014 so a caller that reaches it without a preceding DROP, in
4126
+ -- its own connection or transaction, gets a no-op instead of
4127
+ -- "trigger ... already exists".
4120
4128
  DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_immutable;
4121
- CREATE TRIGGER trg_agent_identity_mapping_history_immutable
4122
- BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
4123
- ON agent_identity_source_mappings
4124
- BEGIN
4125
- SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
4126
- END;
4127
-
4128
4129
  DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_append_only;
4129
- CREATE TRIGGER trg_agent_identity_mapping_history_append_only
4130
- BEFORE DELETE ON agent_identity_source_mappings
4131
- BEGIN
4132
- SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
4133
- END;
4130
+ ${AGENT_IDENTITY_HISTORY_TRIGGER_DDL}
4134
4131
  `);
4135
4132
  db.run("INSERT OR IGNORE INTO _migrations (id) VALUES (?)", [IDENTITY_MIGRATION_ID]);
4136
4133
  });
@@ -4170,7 +4167,20 @@ function storeAgentAlias(agentId, label, aliasKind, status, db) {
4170
4167
  function recordAgentAlias(agentId, label, db) {
4171
4168
  return storeAgentAlias(agentId, label, "historical", "active", db);
4172
4169
  }
4173
- var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65;
4170
+ var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65, AGENT_IDENTITY_HISTORY_TRIGGER_DDL = `
4171
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_immutable
4172
+ BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
4173
+ ON agent_identity_source_mappings
4174
+ BEGIN
4175
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
4176
+ END;
4177
+
4178
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_append_only
4179
+ BEFORE DELETE ON agent_identity_source_mappings
4180
+ BEGIN
4181
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
4182
+ END;
4183
+ `;
4174
4184
  var init_identity_mapping = __esm(() => {
4175
4185
  init_types();
4176
4186
  init_types();
@@ -4195,7 +4205,7 @@ var init_identity_mapping = __esm(() => {
4195
4205
  // src/lib/paths.ts
4196
4206
  import { existsSync as existsSync2 } from "fs";
4197
4207
  import { homedir } from "os";
4198
- import { join, resolve as resolve2 } from "path";
4208
+ import { isAbsolute, join, resolve as resolve2 } from "path";
4199
4209
  import { homedir as pathsResolverHomedir } from "os";
4200
4210
  import { join as pathsResolverJoin } from "path";
4201
4211
  function pathsResolverAssertApp(app) {
@@ -4252,8 +4262,14 @@ function dataDir(options) {
4252
4262
  function effectiveHome(env2 = process.env) {
4253
4263
  return env2.HOME || env2.USERPROFILE || homedir();
4254
4264
  }
4265
+ function hasnaHomeRoot(env2 = process.env) {
4266
+ const override = env2["HASNA_HOME"]?.trim();
4267
+ if (override && isAbsolute(override))
4268
+ return override;
4269
+ return join(effectiveHome(env2), ".hasna");
4270
+ }
4255
4271
  function legacyHomeDir(env2 = process.env) {
4256
- return join(effectiveHome(env2), ".hasna", "todos");
4272
+ return join(hasnaHomeRoot(env2), "todos");
4257
4273
  }
4258
4274
  function resolverHome(env2 = process.env) {
4259
4275
  return dataDir({ app: "todos", home: effectiveHome(env2), env: env2 });
@@ -4672,15 +4688,18 @@ __export(exports_database, {
4672
4688
  resolvePartialId: () => resolvePartialId,
4673
4689
  resolveAssignedToAliases: () => resolveAssignedToAliases,
4674
4690
  resetDatabase: () => resetDatabase,
4691
+ refuseLocalStore: () => refuseLocalStore,
4675
4692
  now: () => now,
4676
4693
  lowerInClause: () => lowerInClause,
4677
4694
  lockExpiryCutoff: () => lockExpiryCutoff,
4678
4695
  isLockExpired: () => isLockExpired,
4696
+ isLocalStoreRefused: () => isLocalStoreRefused,
4679
4697
  getDatabasePath: () => getDatabasePath,
4680
4698
  getDatabase: () => getDatabase,
4681
4699
  closeDatabase: () => closeDatabase,
4682
4700
  clearExpiredLocks: () => clearExpiredLocks,
4683
4701
  assignedToAliasSet: () => assignedToAliasSet,
4702
+ allowLocalStore: () => allowLocalStore,
4684
4703
  LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
4685
4704
  });
4686
4705
  import { Database } from "bun:sqlite";
@@ -4797,7 +4816,18 @@ function maybeInstallShadowCapture(db) {
4797
4816
  console.error(`[todos] shadow capture install failed: ${error instanceof Error ? error.message : String(error)}`);
4798
4817
  }
4799
4818
  }
4819
+ function refuseLocalStore(message) {
4820
+ localStoreRefusal = message;
4821
+ }
4822
+ function allowLocalStore() {
4823
+ localStoreRefusal = null;
4824
+ }
4825
+ function isLocalStoreRefused() {
4826
+ return localStoreRefusal !== null;
4827
+ }
4800
4828
  function getDatabase(dbPath) {
4829
+ if (localStoreRefusal !== null)
4830
+ throw new Error(localStoreRefusal);
4801
4831
  if (dbPath === undefined && !selectsTodosLocalStore()) {
4802
4832
  throw new Error("API_DATABASE_FALLBACK_FORBIDDEN: this operation must use the shared Todos API; implicit SQLite access is unavailable");
4803
4833
  }
@@ -4938,7 +4968,7 @@ function lowerInClause(column, values, params) {
4938
4968
  params.push(...values.map((v) => v.toLowerCase()));
4939
4969
  return `LOWER(${column}) IN (${values.map(() => "?").join(",")})`;
4940
4970
  }
4941
- var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
4971
+ var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, localStoreRefusal = null, ALLOWED_TABLES;
4942
4972
  var init_database = __esm(() => {
4943
4973
  init_local_opt_in();
4944
4974
  init_schema();
@@ -10679,7 +10709,9 @@ function listTasks(filter = {}, db) {
10679
10709
  params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
10680
10710
  } catch {}
10681
10711
  }
10682
- if (!filter.include_archived) {
10712
+ if (filter.archived_only) {
10713
+ conditions.push("archived_at IS NOT NULL");
10714
+ } else if (!filter.include_archived) {
10683
10715
  conditions.push("archived_at IS NULL");
10684
10716
  }
10685
10717
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
@@ -10692,7 +10724,8 @@ function listTasks(filter = {}, db) {
10692
10724
  params.push(filter.offset);
10693
10725
  }
10694
10726
  }
10695
- const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC, id ASC${limitClause}`).all(...params);
10727
+ const orderBy = filter.archived_only ? "archived_at DESC, id DESC" : `${PRIORITY_RANK}, created_at DESC, id ASC`;
10728
+ const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${orderBy}${limitClause}`).all(...params);
10696
10729
  return rows.map(rowToTask);
10697
10730
  }
10698
10731
  function getTaskByFingerprint(fingerprint, db) {
@@ -10835,7 +10868,9 @@ function countTasks(filter = {}, db) {
10835
10868
  }
10836
10869
  }
10837
10870
  addMetadataConditions(filter.metadata, conditions, params);
10838
- if (!filter.include_archived) {
10871
+ if (filter.archived_only) {
10872
+ conditions.push("archived_at IS NOT NULL");
10873
+ } else if (!filter.include_archived) {
10839
10874
  conditions.push("archived_at IS NULL");
10840
10875
  }
10841
10876
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
@@ -10972,6 +11007,10 @@ function updateTaskStored(id, input, db) {
10972
11007
  sets.push("completed_at = ?");
10973
11008
  params.push(input.completed_at);
10974
11009
  }
11010
+ if (input.archived_at !== undefined) {
11011
+ sets.push("archived_at = ?");
11012
+ params.push(input.archived_at);
11013
+ }
10975
11014
  if (input.confidence !== undefined) {
10976
11015
  sets.push("confidence = ?");
10977
11016
  params.push(input.confidence);
@@ -11035,6 +11074,8 @@ function updateTaskStored(id, input, db) {
11035
11074
  logTaskChange(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
11036
11075
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
11037
11076
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
11077
+ if (input.archived_at !== undefined && input.archived_at !== task.archived_at)
11078
+ logTaskChange(id, "update", "archived_at", task.archived_at, input.archived_at, agentId, d);
11038
11079
  if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
11039
11080
  logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
11040
11081
  if (input.approved_by !== undefined)
@@ -13845,7 +13886,7 @@ var init_dispatches = __esm(() => {
13845
13886
  // package.json
13846
13887
  var package_default = {
13847
13888
  name: "@hasna/todos",
13848
- version: "0.16.0",
13889
+ version: "0.17.0",
13849
13890
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
13850
13891
  type: "module",
13851
13892
  main: "dist/index.js",
@@ -13958,7 +13999,7 @@ var package_default = {
13958
13999
  url: "https://github.com/hasna/apps.git",
13959
14000
  directory: "apps/todos"
13960
14001
  },
13961
- homepage: "https://github.com/hasna/apps",
14002
+ homepage: "https://github.com/hasna/apps/tree/main/apps/todos#readme",
13962
14003
  bugs: {
13963
14004
  url: "https://github.com/hasna/apps/issues"
13964
14005
  },
@@ -14381,7 +14422,7 @@ var MCP_TOOL_GROUPS = {
14381
14422
  "machines_topology",
14382
14423
  "machines_unarchive"
14383
14424
  ],
14384
- maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
14425
+ maintenance: ["get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task"]
14385
14426
  };
14386
14427
  var MCP_PROFILE_GROUPS = {
14387
14428
  minimal: ["core", "loops"],
@@ -19806,17 +19847,23 @@ var TODOS_CLI_MCP_PARITY = [
19806
19847
  "todos extract",
19807
19848
  "todos extract-watch"
19808
19849
  ],
19809
- mcpTools: [
19810
- "extract_todos",
19811
- "watch_source_todos"
19812
- ],
19850
+ mcpTools: [],
19813
19851
  jsonContracts: ["source_code_index", "source_todo_comment", "task", "structured_error", "api_error"],
19814
19852
  errorContracts: ["structured_error", "api_error"],
19815
- status: "matched",
19816
- intentionalGaps: [],
19853
+ status: "intentional-gap",
19854
+ intentionalGaps: [
19855
+ {
19856
+ cliCommand: "todos extract",
19857
+ 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)."
19858
+ },
19859
+ {
19860
+ cliCommand: "todos extract-watch",
19861
+ 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."
19862
+ }
19863
+ ],
19864
+ gapReason: "Source scanning and watching are local-filesystem CLI operations; no MCP tool mapping.",
19817
19865
  example: {
19818
- cli: "todos extract . --dry-run --index --json",
19819
- mcpTool: "extract_todos"
19866
+ cli: "todos extract . --dry-run --index --json"
19820
19867
  }
19821
19868
  },
19822
19869
  {
@@ -26405,6 +26452,7 @@ function searchTasks(options, projectId, taskListId, db) {
26405
26452
 
26406
26453
  // src/storage/local-sqlite.ts
26407
26454
  init_tasks();
26455
+ init_task_graph();
26408
26456
  init_projects();
26409
26457
  init_plans();
26410
26458
 
@@ -26968,6 +27016,19 @@ function listAgents(opts, db) {
26968
27016
  }
26969
27017
  return d.query("SELECT * FROM agents WHERE status = 'active' ORDER BY name").all().map(rowToAgent);
26970
27018
  }
27019
+ function listAgentsPage(options, db) {
27020
+ if (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 500) {
27021
+ throw new Error("Agent page limit must be an integer from 1 to 500");
27022
+ }
27023
+ if (!Number.isSafeInteger(options.offset) || options.offset < 0) {
27024
+ throw new Error("Agent page offset must be a non-negative integer");
27025
+ }
27026
+ const d = db || getDatabase();
27027
+ const where = options.include_archived ? "" : "WHERE status = 'active'";
27028
+ const total = d.query(`SELECT COUNT(*) AS total FROM agents ${where}`).get().total;
27029
+ const agents = d.query(`SELECT * FROM agents ${where} ORDER BY LOWER(name), id LIMIT ? OFFSET ?`).all(options.limit, options.offset).map(rowToAgent);
27030
+ return { agents, total };
27031
+ }
26971
27032
  function updateAgentActivity(id, db) {
26972
27033
  const d = db || getDatabase();
26973
27034
  d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), id]);
@@ -28296,6 +28357,98 @@ function exportSqliteMachines(db) {
28296
28357
  }
28297
28358
 
28298
28359
  // src/storage/local-sqlite.ts
28360
+ function bulkCreateAtomicSqlite(db, inputs) {
28361
+ return db.transaction(() => {
28362
+ const tempIds = new Map;
28363
+ const created = [];
28364
+ const dependencies = [];
28365
+ for (const input of inputs) {
28366
+ const { temp_id, depends_on: _dependsOn, ...taskInput } = input;
28367
+ const task2 = createTask(taskInput, db);
28368
+ if (temp_id)
28369
+ tempIds.set(temp_id, task2.id);
28370
+ created.push({ temp_id: temp_id ?? null, id: task2.id, short_id: task2.short_id, title: task2.title });
28371
+ }
28372
+ for (let index = 0;index < inputs.length; index++) {
28373
+ const input = inputs[index];
28374
+ const taskId = created[index].id;
28375
+ const seenDependencies = new Set;
28376
+ for (const reference of input.depends_on ?? []) {
28377
+ const dependencyId = tempIds.get(reference) ?? resolveTaskRefLocal(db, reference)?.id;
28378
+ if (!dependencyId)
28379
+ throw new TaskNotFoundError(reference);
28380
+ if (seenDependencies.has(dependencyId)) {
28381
+ throw new ResourceConflictError("BULK_CREATE_DUPLICATE_DEPENDENCY", `Multiple references resolve to dependency ${dependencyId}`);
28382
+ }
28383
+ seenDependencies.add(dependencyId);
28384
+ addDependency(taskId, dependencyId, db);
28385
+ dependencies.push({ task_id: taskId, depends_on: dependencyId });
28386
+ }
28387
+ }
28388
+ return { schema_version: 1, atomic: true, created, dependencies };
28389
+ })();
28390
+ }
28391
+ function deleteTaskHierarchySqlite(db, rootId) {
28392
+ const rows = db.query(`WITH RECURSIVE tree(id, depth) AS (
28393
+ SELECT id, 0 FROM tasks WHERE id = ?
28394
+ UNION ALL
28395
+ SELECT child.id, tree.depth + 1 FROM tasks child JOIN tree ON child.parent_id = tree.id
28396
+ ) SELECT id FROM tree ORDER BY depth DESC, id`).all(rootId);
28397
+ if (rows.length === 0)
28398
+ throw new TaskNotFoundError(rootId);
28399
+ for (const row of rows) {
28400
+ if (!deleteTask(row.id, db))
28401
+ throw new Error(`Atomic hierarchy delete lost task ${row.id}`);
28402
+ }
28403
+ }
28404
+ function bulkDeleteAtomicSqlite(db, ids2, force) {
28405
+ return db.transaction(() => {
28406
+ const resolved = ids2.map((reference) => ({ reference, task: resolveTaskRefLocal(db, reference) }));
28407
+ const seen = new Set;
28408
+ for (const item of resolved) {
28409
+ if (!item.task)
28410
+ continue;
28411
+ if (seen.has(item.task.id)) {
28412
+ throw new ResourceConflictError("BULK_DELETE_DUPLICATE_TASK", `Multiple references resolve to task ${item.task.id}`);
28413
+ }
28414
+ seen.add(item.task.id);
28415
+ }
28416
+ const childState = new Map;
28417
+ for (const item of resolved) {
28418
+ if (!item.task)
28419
+ continue;
28420
+ childState.set(item.task.id, Boolean(db.query("SELECT id FROM tasks WHERE parent_id = ? LIMIT 1").get(item.task.id)));
28421
+ }
28422
+ const planned = resolved.filter((item) => item.task && (force || !childState.get(item.task.id)));
28423
+ const plannedIds = new Set(planned.map((item) => item.task.id));
28424
+ const roots = planned.filter((item) => {
28425
+ if (!force)
28426
+ return true;
28427
+ let parentId = item.task.parent_id;
28428
+ while (parentId) {
28429
+ if (plannedIds.has(parentId))
28430
+ return false;
28431
+ parentId = getTask(parentId, db)?.parent_id ?? null;
28432
+ }
28433
+ return true;
28434
+ });
28435
+ for (const item of roots)
28436
+ deleteTaskHierarchySqlite(db, item.task.id);
28437
+ return {
28438
+ schema_version: 1,
28439
+ atomic: true,
28440
+ force,
28441
+ results: resolved.map((item) => {
28442
+ if (!item.task)
28443
+ return { requested_id: item.reference, task_id: null, outcome: "missing", reason: "not_found" };
28444
+ if (!force && childState.get(item.task.id)) {
28445
+ return { requested_id: item.reference, task_id: item.task.id, outcome: "skipped", reason: "has_children" };
28446
+ }
28447
+ return { requested_id: item.reference, task_id: item.task.id, outcome: "deleted", reason: null };
28448
+ })
28449
+ };
28450
+ })();
28451
+ }
28299
28452
  var TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
28300
28453
  function resolveTaskRefLocal(db, ref) {
28301
28454
  const raw = ref.trim().toLowerCase();
@@ -28392,6 +28545,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
28392
28545
  },
28393
28546
  handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
28394
28547
  delete: (id) => deleteTask(id, database()),
28548
+ bulkCreateAtomic: (inputs, _context) => bulkCreateAtomicSqlite(database(), inputs),
28549
+ bulkDeleteAtomic: (ids2, force, _context) => bulkDeleteAtomicSqlite(database(), ids2, force),
28395
28550
  start: (id, agentId) => startTask(id, agentId, database()),
28396
28551
  complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
28397
28552
  fail: (id, agentId, reason, options2) => failTask(id, agentId, reason, options2, database()),
@@ -28400,6 +28555,30 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
28400
28555
  getActiveWork: (filters) => getActiveWork(filters, database()),
28401
28556
  getChangedSince: (since, filters) => getTasksChangedSince(since, filters, database())
28402
28557
  },
28558
+ dependencies: {
28559
+ add: (taskId, dependsOn) => {
28560
+ addDependency(taskId, dependsOn, database());
28561
+ return { task_id: taskId, depends_on: dependsOn };
28562
+ },
28563
+ remove: (taskId, dependsOn) => removeDependency(taskId, dependsOn, database()),
28564
+ list: (taskId) => {
28565
+ const dependencies = getTaskDependencies(taskId, database());
28566
+ const blocks = getTaskDependents(taskId, database());
28567
+ return { dependencies, blocks, blocked_by: blocks };
28568
+ },
28569
+ listPage: ({ limit, offset }) => {
28570
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
28571
+ throw new Error("SQLite dependency page limit must be an integer from 1 to 500");
28572
+ }
28573
+ if (!Number.isSafeInteger(offset) || offset < 0) {
28574
+ throw new Error("SQLite dependency page offset must be a non-negative integer");
28575
+ }
28576
+ const totalRow = database().query("SELECT COUNT(*) AS total FROM task_dependencies").get();
28577
+ const dependencies = database().query("SELECT task_id, depends_on FROM task_dependencies ORDER BY task_id, depends_on LIMIT ? OFFSET ?").all(limit, offset);
28578
+ return { dependencies, total: totalRow.total };
28579
+ },
28580
+ listAll: () => database().query("SELECT task_id, depends_on FROM task_dependencies ORDER BY task_id, depends_on").all()
28581
+ },
28403
28582
  projects: {
28404
28583
  create: (input) => createProject(input, database()),
28405
28584
  get: (id) => getProject(id, database()),
@@ -28443,6 +28622,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
28443
28622
  get: (id) => getAgent(id, database()),
28444
28623
  getByName: (name) => getAgentByName(name, database()),
28445
28624
  list: (options2) => listAgents(options2, database()),
28625
+ listPage: (options2) => listAgentsPage(options2, database()),
28446
28626
  update: (id, input) => updateAgent(id, input, database())
28447
28627
  },
28448
28628
  taskLists: {
@@ -28481,6 +28661,29 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
28481
28661
  return comments;
28482
28662
  },
28483
28663
  getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
28664
+ getTaskHistoryPage: (taskId, options2) => {
28665
+ if (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 500) {
28666
+ throw new Error("Task history limit must be an integer from 1 to 500");
28667
+ }
28668
+ if (!Number.isSafeInteger(options2.offset) || options2.offset < 0) {
28669
+ throw new Error("Task history offset must be a non-negative integer");
28670
+ }
28671
+ const conditions = ["task_id = ?"];
28672
+ const values = [taskId];
28673
+ if (options2.since) {
28674
+ conditions.push("created_at >= ?");
28675
+ values.push(options2.since);
28676
+ }
28677
+ if (options2.until) {
28678
+ conditions.push("created_at <= ?");
28679
+ values.push(options2.until);
28680
+ }
28681
+ const where = conditions.join(" AND ");
28682
+ const total = database().query(`SELECT COUNT(*) AS total FROM task_history WHERE ${where}`).get(...values).total;
28683
+ const direction = options2.order === "asc" ? "ASC" : "DESC";
28684
+ 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);
28685
+ return { history, total };
28686
+ },
28484
28687
  getRecentActivity: (limit) => getRecentActivity(limit, database())
28485
28688
  },
28486
28689
  sync: {
@@ -30247,6 +30450,8 @@ function createPostgresTodosStorageAdapter(options) {
30247
30450
  count: (filter = {}) => store.countTasks(filter),
30248
30451
  update: (id, input, context) => updateTask2(id, input, store, context),
30249
30452
  delete: (id, context) => store.deleteTaskHierarchy(id, context),
30453
+ bulkCreateAtomic: (inputs, context) => bulkCreateTasksAtomic(inputs, store, context),
30454
+ bulkDeleteAtomic: (ids2, force, context) => bulkDeleteTasksAtomic(ids2, force, store, context),
30250
30455
  start: (id, agentId) => startTask2(id, agentId, store),
30251
30456
  complete: (id, agentId, options2) => completeTask2(id, agentId, options2, store),
30252
30457
  fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, store),
@@ -30263,6 +30468,7 @@ function createPostgresTodosStorageAdapter(options) {
30263
30468
  add: (taskId, dependsOn, context) => addDependency2(taskId, dependsOn, store, context),
30264
30469
  remove: (taskId, dependsOn) => removeDependency2(taskId, dependsOn, store),
30265
30470
  list: (taskId) => listDependencies(taskId, store),
30471
+ listPage: (page) => store.listDependencyPage(page),
30266
30472
  listAll: () => store.list("dependencies")
30267
30473
  },
30268
30474
  verifications: {
@@ -30328,6 +30534,7 @@ function createPostgresTodosStorageAdapter(options) {
30328
30534
  get: (id) => store.get("agents", id),
30329
30535
  getByName: async (name) => matchAgentByName(await store.list("agents"), name),
30330
30536
  list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
30537
+ listPage: (options2) => store.listAgentPage(options2),
30331
30538
  update: (id, input) => updateAgent2(id, input, store),
30332
30539
  heartbeat: (idOrName, context) => heartbeatAgent(idOrName, store, context),
30333
30540
  release: (idOrName, sessionId, context) => releaseAgent2(idOrName, sessionId, store, context)
@@ -30385,8 +30592,9 @@ function createPostgresTodosStorageAdapter(options) {
30385
30592
  getCommentsPage: async (taskId, options2) => {
30386
30593
  return (await store.listComments(taskId, options2)).map(redactComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
30387
30594
  },
30388
- getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
30389
- getRecentActivity: async (limit = 20) => (await store.list("audit_history")).sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, limit)
30595
+ 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)),
30596
+ getTaskHistoryPage: (taskId, options2) => store.listTaskHistoryPage(taskId, options2),
30597
+ getRecentActivity: (limit = 20) => store.listRecentActivity(limit)
30390
30598
  },
30391
30599
  machines: createPostgresMachineRegistry(options.client, options.service ?? "todos", options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, () => store.ensureSchema()),
30392
30600
  atomicProjectMigration: createAtomicProjectMigration({ client: options.client, table: options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, service: options.service ?? "todos", ensureSchema: () => store.ensureSchema() }),
@@ -30423,6 +30631,8 @@ class PostgresJsonRecordStore {
30423
30631
  }
30424
30632
  async withDependencyGraphTransaction(fn) {
30425
30633
  await this.ensureSchema();
30634
+ if (this.projectIntegrityLocked)
30635
+ return fn(this);
30426
30636
  return this.withTaskParentIntegrityTransaction(async (client) => {
30427
30637
  const scoped = new PostgresJsonRecordStore({ ...this.options, client });
30428
30638
  scoped.schemaReady = Promise.resolve();
@@ -30569,6 +30779,128 @@ class PostgresJsonRecordStore {
30569
30779
  async list(type) {
30570
30780
  return (await this.listRecords(type)).map((record) => record.payload);
30571
30781
  }
30782
+ async listAgentPage(options) {
30783
+ const { limit, offset } = options;
30784
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
30785
+ throw new Error("Postgres agent page limit must be an integer from 1 to 500");
30786
+ }
30787
+ if (!Number.isSafeInteger(offset) || offset < 0) {
30788
+ throw new Error("Postgres agent page offset must be a non-negative integer");
30789
+ }
30790
+ await this.ensureSchema();
30791
+ const result = await this.options.client.query(`/* todos:list-agent-page */ WITH filtered AS (
30792
+ SELECT object_id, payload
30793
+ FROM ${this.tableName}
30794
+ WHERE service = $1 AND object_type = 'agents' AND deleted_at IS NULL
30795
+ AND ($2::boolean OR COALESCE(payload->>'status', 'active') <> 'archived')
30796
+ ), page AS (
30797
+ SELECT object_id, payload FROM filtered
30798
+ ORDER BY LOWER(payload->>'name'), object_id
30799
+ LIMIT $3 OFFSET $4
30800
+ )
30801
+ SELECT (SELECT count(*) FROM filtered) AS total,
30802
+ 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]);
30803
+ const row = result.rows[0];
30804
+ const total = Number(row?.total);
30805
+ if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(row?.agents)) {
30806
+ throw new Error("Postgres agent page returned invalid count or agents");
30807
+ }
30808
+ if (row.agents.length > limit || row.agents.length > 0 && offset + row.agents.length > total) {
30809
+ throw new Error("Postgres agent page exceeded its requested bounds");
30810
+ }
30811
+ return { agents: row.agents.map((value) => payloadRecord2(value)), total };
30812
+ }
30813
+ async listRecentActivity(limit) {
30814
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4) {
30815
+ throw new Error("Postgres recent activity limit must be an integer from 1 to 10000");
30816
+ }
30817
+ await this.ensureSchema();
30818
+ const result = await this.options.client.query(`/* todos:list-recent-activity */ SELECT payload
30819
+ FROM ${this.tableName}
30820
+ WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
30821
+ ORDER BY payload->>'created_at' DESC, object_id DESC
30822
+ LIMIT $2`, [this.service, limit]);
30823
+ return result.rows.map((row) => payloadRecord2(row.payload));
30824
+ }
30825
+ async listTaskHistoryPage(taskId, options) {
30826
+ const { limit, offset } = options;
30827
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
30828
+ throw new Error("Postgres task history limit must be an integer from 1 to 500");
30829
+ }
30830
+ if (!Number.isSafeInteger(offset) || offset < 0) {
30831
+ throw new Error("Postgres task history offset must be a non-negative integer");
30832
+ }
30833
+ await this.ensureSchema();
30834
+ const direction = options.order === "asc" ? "ASC" : "DESC";
30835
+ const result = await this.options.client.query(`/* todos:list-task-history-page */ WITH filtered AS (
30836
+ SELECT object_id, payload, payload->>'created_at' AS created_at
30837
+ FROM ${this.tableName}
30838
+ WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
30839
+ AND payload->>'task_id' = $2
30840
+ AND ($3::text IS NULL OR payload->>'created_at' >= $3)
30841
+ AND ($4::text IS NULL OR payload->>'created_at' <= $4)
30842
+ ), page AS (
30843
+ SELECT object_id, payload, created_at FROM filtered
30844
+ ORDER BY created_at ${direction}, object_id ${direction}
30845
+ LIMIT $5 OFFSET $6
30846
+ )
30847
+ SELECT (SELECT count(*) FROM filtered) AS total,
30848
+ 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]);
30849
+ const row = result.rows[0];
30850
+ const total = Number(row?.total);
30851
+ if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(row?.history)) {
30852
+ throw new Error("Postgres task history page returned invalid count or history");
30853
+ }
30854
+ if (row.history.length > limit || row.history.length > 0 && offset + row.history.length > total) {
30855
+ throw new Error("Postgres task history page exceeded its requested bounds");
30856
+ }
30857
+ return { history: row.history.map((value) => payloadRecord2(value)), total };
30858
+ }
30859
+ async listDependencyPage(options) {
30860
+ const { limit, offset } = options;
30861
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
30862
+ throw new Error("Postgres dependency page limit must be an integer from 1 to 500");
30863
+ }
30864
+ if (!Number.isSafeInteger(offset) || offset < 0) {
30865
+ throw new Error("Postgres dependency page offset must be a non-negative integer");
30866
+ }
30867
+ await this.ensureSchema();
30868
+ const result = await this.options.client.query(`/* todos:list-dependencies-page */ WITH page AS (
30869
+ SELECT payload, updated_at, object_id
30870
+ FROM ${this.tableName}
30871
+ WHERE service = $1 AND object_type = 'dependencies' AND deleted_at IS NULL
30872
+ ORDER BY updated_at ASC, object_id ASC
30873
+ LIMIT $2 OFFSET $3
30874
+ )
30875
+ SELECT
30876
+ (SELECT COUNT(*)::text FROM ${this.tableName}
30877
+ WHERE service = $1 AND object_type = 'dependencies' AND deleted_at IS NULL) AS total,
30878
+ COALESCE(
30879
+ jsonb_agg(page.payload ORDER BY page.updated_at ASC, page.object_id ASC)
30880
+ FILTER (WHERE page.payload IS NOT NULL),
30881
+ '[]'::jsonb
30882
+ ) AS dependencies
30883
+ FROM page`, [this.service, limit, offset]);
30884
+ const row = result.rows[0];
30885
+ const totalText = typeof row?.total === "string" ? row.total : typeof row?.total === "number" && Number.isSafeInteger(row.total) ? String(row.total) : null;
30886
+ if (totalText === null || !/^\d+$/.test(totalText)) {
30887
+ throw new Error("Postgres dependency page returned an invalid total");
30888
+ }
30889
+ const total = Number(totalText);
30890
+ if (!Number.isSafeInteger(total)) {
30891
+ throw new Error("Postgres dependency page total exceeds the safe integer range");
30892
+ }
30893
+ if (!Array.isArray(row?.dependencies)) {
30894
+ throw new Error("Postgres dependency page returned an invalid dependencies array");
30895
+ }
30896
+ if (row.dependencies.length > limit || offset + row.dependencies.length > total) {
30897
+ throw new Error("Postgres dependency page returned rows outside the requested bound");
30898
+ }
30899
+ return {
30900
+ dependencies: row.dependencies.map((value) => payloadRecord2(value)),
30901
+ total
30902
+ };
30903
+ }
30572
30904
  async listComments(taskId, options = {}) {
30573
30905
  await this.ensureSchema();
30574
30906
  const limit = options.limit ?? 100;
@@ -30644,7 +30976,9 @@ class PostgresJsonRecordStore {
30644
30976
  return "1=0";
30645
30977
  return `${column} IN (${values.map((v) => p(v)).join(", ")})`;
30646
30978
  };
30647
- if (filter.include_archived === false)
30979
+ if (filter.archived_only === true)
30980
+ conds.push(`(payload->>'archived_at' IS NOT NULL)`);
30981
+ else if (filter.include_archived === false)
30648
30982
  conds.push(`(payload->>'archived_at' IS NULL)`);
30649
30983
  if (filter.ids)
30650
30984
  conds.push(inClause("payload->>'id'", filter.ids));
@@ -30701,7 +31035,7 @@ class PostgresJsonRecordStore {
30701
31035
  async listTasks(filter) {
30702
31036
  await this.ensureSchema();
30703
31037
  const { where, params, queryRef } = await this.buildTaskFilterSql(filter);
30704
- 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;
31038
+ 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;
30705
31039
  let sql = `/* todos:list-tasks */ SELECT payload FROM ${this.tableName} WHERE ${where} ${orderBy}`;
30706
31040
  if (filter.limit !== undefined) {
30707
31041
  params.push(filter.limit);
@@ -30921,6 +31255,8 @@ class PostgresJsonRecordStore {
30921
31255
  if (planIds.length === 0 && !parentGuard)
30922
31256
  return this.upsert("tasks", value, context);
30923
31257
  await this.ensureSchema();
31258
+ if (!queryClient && this.projectIntegrityLocked)
31259
+ queryClient = this.options.client;
30924
31260
  if (!queryClient) {
30925
31261
  return this.withTaskParentIntegrityTransaction((client2) => this.upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context, parentGuard, client2));
30926
31262
  }
@@ -31458,9 +31794,11 @@ class PostgresJsonRecordStore {
31458
31794
  }
31459
31795
  async deleteTaskHierarchy(id, context = {}) {
31460
31796
  await this.ensureSchema();
31461
- return this.withTaskParentIntegrityTransaction(async (client) => {
31462
- const timestamp3 = new Date().toISOString();
31463
- const result = await client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
31797
+ if (!this.projectIntegrityLocked) {
31798
+ return this.withDependencyGraphTransaction((scoped) => scoped.deleteTaskHierarchy(id, context));
31799
+ }
31800
+ const timestamp3 = new Date().toISOString();
31801
+ const result = await this.options.client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
31464
31802
  task_tree(object_id, path, cycle) AS (
31465
31803
  SELECT task.object_id, ARRAY[task.object_id], false
31466
31804
  FROM ${this.tableName} AS task
@@ -31522,13 +31860,12 @@ class PostgresJsonRecordStore {
31522
31860
  SELECT EXISTS (SELECT 1 FROM task_tree WHERE object_id = $2) AS found,
31523
31861
  (SELECT count(*) FROM tombstoned) AS deleted_count,
31524
31862
  (SELECT count(*) FROM tombstoned_related) AS related_deleted_count`, [
31525
- this.service,
31526
- id,
31527
- timestamp3,
31528
- context.requestId ?? this.sourceMachineId ?? null
31529
- ]);
31530
- return Boolean(result.rows[0]?.found);
31531
- });
31863
+ this.service,
31864
+ id,
31865
+ timestamp3,
31866
+ context.requestId ?? this.sourceMachineId ?? null
31867
+ ]);
31868
+ return Boolean(result.rows[0]?.found);
31532
31869
  }
31533
31870
  async getPlanProjectLinkReceipt(receiptId) {
31534
31871
  const value = await this.get("plan_project_link_receipts", receiptId);
@@ -31984,6 +32321,102 @@ class PostgresJsonRecordStore {
31984
32321
  updated_at = EXCLUDED.updated_at`, [this.service, name, value]);
31985
32322
  }
31986
32323
  }
32324
+ async function bulkCreateTasksAtomic(inputs, store, context) {
32325
+ return store.withDependencyGraphTransaction(async (scoped) => {
32326
+ const tempIds = new Map;
32327
+ const created = [];
32328
+ const dependencies = [];
32329
+ for (const input of inputs) {
32330
+ const { temp_id, depends_on: _dependsOn, ...taskInput } = input;
32331
+ const task2 = await createTask2(taskInput, scoped, context);
32332
+ if (temp_id)
32333
+ tempIds.set(temp_id, task2.id);
32334
+ created.push({ temp_id: temp_id ?? null, id: task2.id, short_id: task2.short_id, title: task2.title });
32335
+ }
32336
+ for (let index = 0;index < inputs.length; index++) {
32337
+ const input = inputs[index];
32338
+ const taskId = created[index].id;
32339
+ const seenDependencies = new Set;
32340
+ for (const reference of input.depends_on ?? []) {
32341
+ const exact = tempIds.get(reference) ?? (await scoped.get("tasks", reference))?.id;
32342
+ const dependencyId = exact ?? (await scoped.resolveTaskRef(reference))?.id;
32343
+ if (!dependencyId)
32344
+ throw new TaskNotFoundError(reference);
32345
+ if (seenDependencies.has(dependencyId)) {
32346
+ throw new ResourceConflictError("BULK_CREATE_DUPLICATE_DEPENDENCY", `Multiple references resolve to dependency ${dependencyId}`);
32347
+ }
32348
+ seenDependencies.add(dependencyId);
32349
+ dependencies.push(await addDependency2(taskId, dependencyId, scoped, context));
32350
+ }
32351
+ }
32352
+ return { schema_version: 1, atomic: true, created, dependencies };
32353
+ });
32354
+ }
32355
+ async function bulkDeleteTasksAtomic(ids2, force, store, context) {
32356
+ return store.withDependencyGraphTransaction(async (scoped) => {
32357
+ const resolved = [];
32358
+ for (const reference of ids2) {
32359
+ resolved.push({
32360
+ reference,
32361
+ task: await scoped.get("tasks", reference) ?? await scoped.resolveTaskRef(reference)
32362
+ });
32363
+ }
32364
+ const seen = new Set;
32365
+ for (const item of resolved) {
32366
+ if (!item.task)
32367
+ continue;
32368
+ if (seen.has(item.task.id)) {
32369
+ throw new ResourceConflictError("BULK_DELETE_DUPLICATE_TASK", `Multiple references resolve to task ${item.task.id}`);
32370
+ }
32371
+ seen.add(item.task.id);
32372
+ }
32373
+ const childState = new Map;
32374
+ for (const item of resolved) {
32375
+ if (!item.task)
32376
+ continue;
32377
+ const children = await scoped.listTasks({ parent_id: item.task.id, include_subtasks: true, limit: 1 });
32378
+ childState.set(item.task.id, children.length > 0);
32379
+ }
32380
+ const planned = resolved.filter((item) => item.task && (force || !childState.get(item.task.id)));
32381
+ const plannedIds = new Set(planned.map((item) => item.task.id));
32382
+ const roots = [];
32383
+ for (const item of planned) {
32384
+ if (!force) {
32385
+ roots.push(item);
32386
+ continue;
32387
+ }
32388
+ let parentId = item.task.parent_id;
32389
+ let covered = false;
32390
+ while (parentId) {
32391
+ if (plannedIds.has(parentId)) {
32392
+ covered = true;
32393
+ break;
32394
+ }
32395
+ parentId = (await scoped.get("tasks", parentId))?.parent_id ?? null;
32396
+ }
32397
+ if (!covered)
32398
+ roots.push(item);
32399
+ }
32400
+ for (const item of roots) {
32401
+ if (!await scoped.deleteTaskHierarchy(item.task.id, context)) {
32402
+ throw new Error(`Atomic bulk delete lost task ${item.task.id}`);
32403
+ }
32404
+ }
32405
+ return {
32406
+ schema_version: 1,
32407
+ atomic: true,
32408
+ force,
32409
+ results: resolved.map((item) => {
32410
+ if (!item.task)
32411
+ return { requested_id: item.reference, task_id: null, outcome: "missing", reason: "not_found" };
32412
+ if (!force && childState.get(item.task.id)) {
32413
+ return { requested_id: item.reference, task_id: item.task.id, outcome: "skipped", reason: "has_children" };
32414
+ }
32415
+ return { requested_id: item.reference, task_id: item.task.id, outcome: "deleted", reason: null };
32416
+ })
32417
+ };
32418
+ });
32419
+ }
31987
32420
  async function createTask2(input, store, context) {
31988
32421
  const timestamp3 = new Date().toISOString();
31989
32422
  const taskId = randomUUID4();
@@ -32074,12 +32507,16 @@ async function updateTask2(id, input, store, context) {
32074
32507
  }
32075
32508
  }
32076
32509
  }
32510
+ const patchTouchesPlacement = input.project_id !== undefined || input.task_list_id !== undefined;
32511
+ const effectiveProjectIdForShortId = input.project_id !== undefined ? input.project_id : existing.project_id;
32512
+ const backfilledShortId = existing.short_id === null && patchTouchesPlacement && effectiveProjectIdForShortId ? await nextTaskShortId2(effectiveProjectIdForShortId, store, context) : null;
32077
32513
  const reopened = existing.status === "completed" && input.status !== undefined && input.status !== "completed" && input.completed_at === undefined;
32078
32514
  const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
32079
32515
  const task2 = {
32080
32516
  ...existing,
32081
32517
  ...definedPatch(input),
32082
32518
  ...terminalNow ? { locked_by: null, locked_at: null } : {},
32519
+ short_id: backfilledShortId ?? existing.short_id,
32083
32520
  version: existing.version + 1,
32084
32521
  updated_at: new Date().toISOString(),
32085
32522
  tags: input.tags ?? existing.tags,
@@ -34730,15 +35167,16 @@ function assertTodosPriorRegistrationAdoptionValidationEnvelope(value, input) {
34730
35167
  // src/sdk/resolve.ts
34731
35168
  init_local_opt_in();
34732
35169
  var TODOS_LOCAL_SERVE_URL = "http://localhost:19427";
35170
+ var TODOS_CREDENTIAL_MISSING_MESSAGE = "TODOS_CREDENTIAL_MISSING: no Hasna Todos credential resolved. Looked at " + "HASNA_TODOS_API_KEY_OVERRIDE / HASNA_PROFILE / HASNA_TODOS_API_KEY_REF, the Keychain item " + "hasna.credentials.todos.api-key, ~/.hasna/todos/config/credentials, then HASNA_TODOS_API_KEY. " + "There is no local fallback: the on-box todos-serve is opt-in only (HASNA_TODOS_LOCAL=1, alias " + "TODOS_LOCAL=1) and is disabled by default \u2014 failing closed.";
34733
35171
  var localNoticePrinted = false;
34734
35172
  function stripV1(baseUrl) {
34735
35173
  return baseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
34736
35174
  }
34737
- function announceLocal(notice, reason) {
35175
+ function announceLocal(notice) {
34738
35176
  if (localNoticePrinted)
34739
35177
  return;
34740
35178
  localNoticePrinted = true;
34741
- const line = `todos: LOCAL mode \u2014 no Hasna credential resolved (${reason}); reading and writing the local ` + `todos-serve at ${TODOS_LOCAL_SERVE_URL}, not the hosted fleet. Set HASNA_TODOS_API_KEY, add the ` + `Keychain item hasna.credentials.todos.api-key, or write ~/.hasna/todos/config/credentials to go hosted.`;
35179
+ const line = `todos: LOCAL mode \u2014 HASNA_TODOS_LOCAL is set and nothing configures a Todos authority; reading and ` + `writing the local todos-serve at ${TODOS_LOCAL_SERVE_URL}, not the hosted fleet. 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.`;
34742
35180
  if (notice)
34743
35181
  notice(line);
34744
35182
  else if (typeof process !== "undefined")
@@ -34762,7 +35200,7 @@ function resolveTodosSdkTransport(options = {}) {
34762
35200
  };
34763
35201
  }
34764
35202
  if (selectsTodosLocalStore(env2)) {
34765
- announceLocal(options.notice, "HASNA_TODOS_LOCAL is set and nothing configures an authority");
35203
+ announceLocal(options.notice);
34766
35204
  return {
34767
35205
  mode: "local-serve",
34768
35206
  baseUrl: TODOS_LOCAL_SERVE_URL,
@@ -34780,14 +35218,7 @@ function resolveTodosSdkTransport(options = {}) {
34780
35218
  resolution = resolveClientTransport("todos", env2, chainOptions);
34781
35219
  } catch (error) {
34782
35220
  if (error instanceof ClientTransportConfigurationError && /is not set and no API key could be resolved/.test(error.message)) {
34783
- announceLocal(options.notice, "nothing configured a Hasna Todos credential");
34784
- return {
34785
- mode: "local-serve",
34786
- baseUrl: TODOS_LOCAL_SERVE_URL,
34787
- apiKey: null,
34788
- apiKeySource: null,
34789
- apiUrlSource: "local-serve"
34790
- };
35221
+ throw new Error(`${TODOS_CREDENTIAL_MISSING_MESSAGE} ${error.message}`, { cause: error });
34791
35222
  }
34792
35223
  throw error;
34793
35224
  }
@@ -48211,6 +48642,10 @@ function rethrowAuthorityFailure(error) {
48211
48642
  throw new Error(`REMOTE_API_URL_INVALID: ${message} local SQLite fallback is disabled`, { cause: error });
48212
48643
  }
48213
48644
  function classifyRemoteRequestError(baseUrl, route, error) {
48645
+ const errorName = error instanceof Error ? error.name : "";
48646
+ if (errorName === "CredentialResolutionError" || errorName === "CredentialFileUnsafeError") {
48647
+ rethrowAuthorityFailure(error);
48648
+ }
48214
48649
  const status3 = error && typeof error === "object" ? error.status : undefined;
48215
48650
  if (status3 === 401) {
48216
48651
  throw new Error(`REMOTE_API_UNAUTHORIZED: configured Todos authority ${baseUrl} rejected HASNA_TODOS_API_KEY for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
@@ -48359,6 +48794,10 @@ function toListQuery(filter = {}) {
48359
48794
  query["parent_id"] = filter.parent_id ?? "";
48360
48795
  if (filter.include_subtasks !== undefined)
48361
48796
  query["include_subtasks"] = filter.include_subtasks ? "true" : "false";
48797
+ if (filter.include_archived !== undefined)
48798
+ query["include_archived"] = filter.include_archived ? "true" : "false";
48799
+ if (filter.archived_only !== undefined)
48800
+ query["archived_only"] = filter.archived_only ? "true" : "false";
48362
48801
  if (filter.plan_id)
48363
48802
  query["plan_id"] = filter.plan_id;
48364
48803
  if (filter.task_list_id)
@@ -54871,7 +55310,7 @@ function formatTraceabilityReport(report) {
54871
55310
  }
54872
55311
  // src/lib/mention-resolver.ts
54873
55312
  import { existsSync as existsSync17, readdirSync as readdirSync3, readFileSync as readFileSync13, statSync as statSync6 } from "fs";
54874
- import { basename as basename4, isAbsolute, join as join15, relative as relative4, resolve as resolve17, sep as sep2 } from "path";
55313
+ import { basename as basename4, isAbsolute as isAbsolute2, join as join15, relative as relative4, resolve as resolve17, sep as sep2 } from "path";
54875
55314
  init_database();
54876
55315
  init_plans();
54877
55316
  init_task_runs();
@@ -54954,7 +55393,7 @@ function normalizeWorkspace(workspace) {
54954
55393
  }
54955
55394
  function isInside(root, absolutePath) {
54956
55395
  const rel = relative4(root, absolutePath);
54957
- return rel === "" || !rel.startsWith("..") && !rel.includes(`..${sep2}`) && !isAbsolute(rel);
55396
+ return rel === "" || !rel.startsWith("..") && !rel.includes(`..${sep2}`) && !isAbsolute2(rel);
54958
55397
  }
54959
55398
  function normalizeRelativePath(value) {
54960
55399
  const normalized2 = value.trim().replace(/^\.?\//, "").replace(/\\/g, "/");
@@ -64038,7 +64477,6 @@ var ALL_MCP_TOOLS = [
64038
64477
  "export_todos_md",
64039
64478
  "export_verification_evidence",
64040
64479
  "extend_task",
64041
- "extract_todos",
64042
64480
  "fail_agent_run",
64043
64481
  "fail_task",
64044
64482
  "find_duplicate_tasks",