@hasna/todos 0.13.9 → 0.13.11

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.
package/dist/mcp/index.js CHANGED
@@ -44,7 +44,7 @@ var __require = import.meta.require;
44
44
  function isBlockingDependencyStatus(status) {
45
45
  return status !== "completed" && status !== "cancelled";
46
46
  }
47
- var TASK_STATUSES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
47
+ var TASK_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
48
48
  var init_types = __esm(() => {
49
49
  TASK_STATUSES = [
50
50
  "pending",
@@ -77,6 +77,20 @@ var init_types = __esm(() => {
77
77
  this.name = "TaskNotFoundError";
78
78
  }
79
79
  };
80
+ TaskNotStartableError = class TaskNotStartableError extends Error {
81
+ taskId;
82
+ status;
83
+ agentId;
84
+ static code = "TASK_NOT_STARTABLE";
85
+ static suggestion = "Reset the task status to pending before starting it again.";
86
+ constructor(taskId, status, agentId) {
87
+ super(`Task ${taskId} is ${status} and cannot be started by ${agentId}; ` + "reset the task status to pending before starting it again");
88
+ this.taskId = taskId;
89
+ this.status = status;
90
+ this.agentId = agentId;
91
+ this.name = "TaskNotStartableError";
92
+ }
93
+ };
80
94
  TaskReferenceAmbiguousError = class TaskReferenceAmbiguousError extends Error {
81
95
  reference;
82
96
  static code = "TASK_REFERENCE_AMBIGUOUS";
@@ -5095,6 +5109,24 @@ var init_agents = __esm(() => {
5095
5109
  init_agent_names();
5096
5110
  });
5097
5111
 
5112
+ // src/lib/lock-display.ts
5113
+ function lockDisplayState(lockedBy, lockedAt, nowMs = Date.now()) {
5114
+ const holder = lockedBy ?? null;
5115
+ const at = lockedAt ?? null;
5116
+ if (!holder) {
5117
+ return { present: false, held: false, expired: false, holder: null, lockedAt: at };
5118
+ }
5119
+ const expired = isLockExpired(at, nowMs);
5120
+ return { present: true, held: !expired, expired, holder, lockedAt: at };
5121
+ }
5122
+ function formatExpiredLock(state) {
5123
+ const at = state.lockedAt ? ` at ${state.lockedAt}` : "";
5124
+ return `expired (last held by ${state.holder}${at})`;
5125
+ }
5126
+ var init_lock_display = __esm(() => {
5127
+ init_database();
5128
+ });
5129
+
5098
5130
  // src/lib/logger.ts
5099
5131
  async function logError(_message, _opts) {}
5100
5132
 
@@ -12865,7 +12897,7 @@ function assertStartable(task, agentId) {
12865
12897
  return;
12866
12898
  if (task.status === "in_progress")
12867
12899
  return;
12868
- throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
12900
+ throw new TaskNotStartableError(task.id, task.status, agentId);
12869
12901
  }
12870
12902
  function getBlockingDeps(id, db) {
12871
12903
  const d = db || getDatabase();
@@ -12936,7 +12968,7 @@ function completeTask(id, agentId, db, options) {
12936
12968
  if (task.status === "cancelled") {
12937
12969
  throw new Error(`Task ${id} is cancelled and cannot be completed`);
12938
12970
  }
12939
- if (agentId && task.locked_by && !sameHolder(task.locked_by, agentId) && !isLockExpired(task.locked_at)) {
12971
+ if (task.locked_by && !sameHolder(task.locked_by, agentId) && !isLockExpired(task.locked_at)) {
12940
12972
  throw new LockError(id, task.locked_by);
12941
12973
  }
12942
12974
  checkCompletionGuard(task, agentId || null, d);
@@ -13571,6 +13603,30 @@ function getTaskWithRelations(id, db) {
13571
13603
  checklist
13572
13604
  };
13573
13605
  }
13606
+ function resolveAssignedToAliases(db, ref) {
13607
+ const aliases = new Set([ref]);
13608
+ let agentId;
13609
+ try {
13610
+ agentId = resolvePartialId(db, "agents", ref);
13611
+ } catch (err) {
13612
+ if (!(err instanceof IdentityAliasAmbiguousError))
13613
+ throw err;
13614
+ agentId = null;
13615
+ }
13616
+ if (agentId) {
13617
+ aliases.add(agentId);
13618
+ const row = db.query("SELECT name FROM agents WHERE id = ?").get(agentId);
13619
+ if (row?.name)
13620
+ aliases.add(row.name);
13621
+ }
13622
+ return [...aliases];
13623
+ }
13624
+ function lowerInClause(column, values, params) {
13625
+ if (values.length === 0)
13626
+ return "1=0";
13627
+ params.push(...values.map((v) => v.toLowerCase()));
13628
+ return `LOWER(${column}) IN (${values.map(() => "?").join(",")})`;
13629
+ }
13574
13630
  function listTasks(filter = {}, db) {
13575
13631
  const d = db || getDatabase();
13576
13632
  const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
@@ -13612,8 +13668,7 @@ function listTasks(filter = {}, db) {
13612
13668
  }
13613
13669
  }
13614
13670
  if (filter.assigned_to) {
13615
- conditions.push("assigned_to = ?");
13616
- params.push(filter.assigned_to);
13671
+ conditions.push(lowerInClause("assigned_to", resolveAssignedToAliases(d, filter.assigned_to), params));
13617
13672
  }
13618
13673
  if (filter.agent_id) {
13619
13674
  conditions.push("agent_id = ?");
@@ -13773,8 +13828,7 @@ function countTasks(filter = {}, db) {
13773
13828
  }
13774
13829
  }
13775
13830
  if (filter.assigned_to) {
13776
- conditions.push("assigned_to = ?");
13777
- params.push(filter.assigned_to);
13831
+ conditions.push(lowerInClause("assigned_to", resolveAssignedToAliases(d, filter.assigned_to), params));
13778
13832
  }
13779
13833
  if (filter.agent_id) {
13780
13834
  conditions.push("agent_id = ?");
@@ -17958,10 +18012,11 @@ function validateAssignee(input, ctx) {
17958
18012
  if (ctx.allowSeat) {
17959
18013
  return { ok: true, assignee: byId ? byId.name : raw, agentId: byId?.id, isSeat: true };
17960
18014
  }
18015
+ const hint = ctx.seatHint?.(raw);
17961
18016
  return {
17962
18017
  ok: false,
17963
18018
  reason: "seat",
17964
- message: `'${raw}' is a durable SEAT, and a task assigned to a seat is assigned to nobody \u2014 no session is watching that queue. ` + `Assign a specific agent, use --unassigned to file it with no owner on purpose, or pass --assign-seat if filing at the seat is what you mean.`
18019
+ message: `'${raw}' is a durable SEAT, and a task assigned to a seat is assigned to nobody \u2014 no session is watching that queue. ` + `Assign a specific agent, use --unassigned to file it with no owner on purpose, or repeat the command with --assign-seat added to confirm the seat is deliberate` + (hint ? `: ${hint}` : " (see --help for this command's exact form).")
17965
18020
  };
17966
18021
  }
17967
18022
  if (byId) {
@@ -20410,6 +20465,16 @@ function classifyRemoteRequestError(baseUrl, route, error) {
20410
20465
  if (status === 403) {
20411
20466
  throw new Error(`REMOTE_API_FORBIDDEN: configured Todos authority ${baseUrl} denied ${route}; local SQLite fallback is disabled`, { cause: error });
20412
20467
  }
20468
+ if (status === 409) {
20469
+ const body = error && typeof error === "object" ? error.body : undefined;
20470
+ if (body && typeof body === "object" && !Array.isArray(body)) {
20471
+ const remoteError = body;
20472
+ if (remoteError.code === "TASK_NOT_STARTABLE" && typeof remoteError.error === "string" && remoteError.error.length > 0) {
20473
+ throw new Error(`${remoteError.code}: ${remoteError.error}`, { cause: error });
20474
+ }
20475
+ }
20476
+ throw error;
20477
+ }
20413
20478
  if (typeof status === "number" && status >= 300 && status < 400) {
20414
20479
  throw new Error(`REMOTE_API_REDIRECT_REJECTED: configured Todos authority ${baseUrl} redirected ${route}; ` + "authenticated redirects are disabled to prevent credential leakage", { cause: error });
20415
20480
  }
@@ -20607,7 +20672,10 @@ function resolveCloudProjectRef(projects, ref) {
20607
20672
  if (matches.length > 1)
20608
20673
  throw new Error(`Project reference is ambiguous: "${input}"`);
20609
20674
  }
20610
- throw new Error(`Project not found: "${input}"`);
20675
+ if (UUID_RE.test(input) || /^[0-9a-f]{4,}$/i.test(input)) {
20676
+ throw new Error(`Project not found: "${input}"`);
20677
+ }
20678
+ throw new Error(`Project not found: "${input}" \u2014 no project in todos's own project registry matches this ` + `name, path, or slug. This does not know about projects registered elsewhere (for example ` + `a Hasna Projects CLI workspace) \u2014 resolve it there first (e.g. "projects show ${input} ` + `--json") and pass the resulting id or task-list slug, or run "todos projects create" to ` + `register it here. Do not assume "${input}" has never been created.`);
20611
20679
  }
20612
20680
  async function cloudResolveProjectRef(client, ref) {
20613
20681
  return resolveCloudProjectRef(await cloudListProjects(client), ref);
@@ -34775,7 +34843,7 @@ var package_default;
34775
34843
  var init_package = __esm(() => {
34776
34844
  package_default = {
34777
34845
  name: "@hasna/todos",
34778
- version: "0.13.9",
34846
+ version: "0.13.11",
34779
34847
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
34780
34848
  type: "module",
34781
34849
  main: "dist/index.js",
@@ -34825,8 +34893,8 @@ var init_package = __esm(() => {
34825
34893
  "README.md"
34826
34894
  ],
34827
34895
  scripts: {
34828
- build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
34829
- "build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
34896
+ build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
34897
+ "build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
34830
34898
  migrate: "bun run src/server/index.ts migrate",
34831
34899
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
34832
34900
  "generate:sdk": "bun run scripts/generate-sdk.ts",
@@ -45719,7 +45787,16 @@ class PostgresJsonRecordStore {
45719
45787
  updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString()
45720
45788
  }));
45721
45789
  }
45722
- buildTaskFilterSql(filter) {
45790
+ async resolveAssignedToAliases(ref) {
45791
+ const agent = await resolveAgentForAssignedFilter(ref, this);
45792
+ const aliases = new Set([ref]);
45793
+ if (agent) {
45794
+ aliases.add(agent.id);
45795
+ aliases.add(agent.name);
45796
+ }
45797
+ return [...aliases];
45798
+ }
45799
+ async buildTaskFilterSql(filter) {
45723
45800
  const params = [this.service, "tasks"];
45724
45801
  const conds = ["service = $1", "object_type = $2", "deleted_at IS NULL"];
45725
45802
  const p = (value) => {
@@ -45745,8 +45822,10 @@ class PostgresJsonRecordStore {
45745
45822
  conds.push(inClause("payload->>'status'", toFilterArray(filter.status)));
45746
45823
  if (filter.priority !== undefined)
45747
45824
  conds.push(inClause("payload->>'priority'", toFilterArray(filter.priority)));
45748
- if (filter.assigned_to !== undefined)
45749
- conds.push(`payload->>'assigned_to' = ${p(filter.assigned_to)}`);
45825
+ if (filter.assigned_to !== undefined) {
45826
+ const aliases = [...new Set((await this.resolveAssignedToAliases(filter.assigned_to)).map((a) => a.toLowerCase()))];
45827
+ conds.push(inClause("LOWER(payload->>'assigned_to')", aliases));
45828
+ }
45750
45829
  if (filter.agent_id !== undefined)
45751
45830
  conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
45752
45831
  if (filter.created_by !== undefined)
@@ -45780,7 +45859,7 @@ class PostgresJsonRecordStore {
45780
45859
  }
45781
45860
  async listTasks(filter) {
45782
45861
  await this.ensureSchema();
45783
- const { where, params, queryRef } = this.buildTaskFilterSql(filter);
45862
+ const { where, params, queryRef } = await this.buildTaskFilterSql(filter);
45784
45863
  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;
45785
45864
  let sql = `/* todos:list-tasks */ SELECT payload FROM ${this.tableName} WHERE ${where} ${orderBy}`;
45786
45865
  if (filter.limit !== undefined) {
@@ -45840,7 +45919,7 @@ class PostgresJsonRecordStore {
45840
45919
  }
45841
45920
  async countTasks(filter) {
45842
45921
  await this.ensureSchema();
45843
- const { where, params } = this.buildTaskFilterSql(filter);
45922
+ const { where, params } = await this.buildTaskFilterSql(filter);
45844
45923
  const sql = `/* todos:count-tasks */ SELECT COUNT(*)::int AS count FROM ${this.tableName} WHERE ${where}`;
45845
45924
  const result = await this.options.client.query(sql, params);
45846
45925
  return Number(result.rows[0]?.count ?? 0);
@@ -46001,9 +46080,13 @@ class PostgresJsonRecordStore {
46001
46080
  } : {};
46002
46081
  const hasEvidence = Object.keys(evidence).length > 0;
46003
46082
  const hasConfidence = options?.confidence !== undefined;
46004
- const result = await this.options.client.query(`/* todos:complete-task-atomic */ UPDATE ${this.tableName}
46083
+ const lockExpiryCutoff2 = new Date(new Date(operationTimestamp).getTime() - CLOUD_LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
46084
+ const result = await this.options.client.query(`/* todos:complete-task-atomic todos:complete-task-lock-guard todos:complete-task-clears-lock */
46085
+ UPDATE ${this.tableName}
46005
46086
  SET payload = payload || jsonb_build_object(
46006
46087
  'status', 'completed',
46088
+ 'locked_by', 'null'::jsonb,
46089
+ 'locked_at', 'null'::jsonb,
46007
46090
  'assigned_to', CASE
46008
46091
  WHEN jsonb_typeof(payload->'assigned_to') = 'string' THEN payload->'assigned_to'
46009
46092
  ELSE COALESCE(to_jsonb($3::text), 'null'::jsonb)
@@ -46031,6 +46114,16 @@ class PostgresJsonRecordStore {
46031
46114
  updated_at = $9::timestamptz,
46032
46115
  version = COALESCE(version, 0) + 1
46033
46116
  WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL
46117
+ AND (
46118
+ payload->>'locked_by' IS NULL
46119
+ OR BTRIM(payload->>'locked_by') = ''
46120
+ OR payload->>'locked_at' IS NULL
46121
+ OR payload->>'locked_at' < $10::text
46122
+ OR (
46123
+ $3::text IS NOT NULL
46124
+ AND LOWER(BTRIM(payload->>'locked_by')) = LOWER(BTRIM($3::text))
46125
+ )
46126
+ )
46034
46127
  RETURNING payload`, [
46035
46128
  this.service,
46036
46129
  id,
@@ -46040,7 +46133,8 @@ class PostgresJsonRecordStore {
46040
46133
  jsonbParam(evidence),
46041
46134
  hasConfidence,
46042
46135
  options?.confidence ?? null,
46043
- operationTimestamp
46136
+ operationTimestamp,
46137
+ lockExpiryCutoff2
46044
46138
  ]);
46045
46139
  return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
46046
46140
  }
@@ -46321,6 +46415,7 @@ async function updateTask2(id, input, store) {
46321
46415
  if (existing.version !== input.version) {
46322
46416
  throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
46323
46417
  }
46418
+ const reopened = existing.status === "completed" && input.status !== undefined && input.status !== "completed" && input.completed_at === undefined;
46324
46419
  const task2 = {
46325
46420
  ...existing,
46326
46421
  ...definedPatch(input),
@@ -46330,7 +46425,8 @@ async function updateTask2(id, input, store) {
46330
46425
  metadata: input.metadata ?? existing.metadata,
46331
46426
  requires_approval: input.requires_approval ?? existing.requires_approval,
46332
46427
  task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
46333
- created_by: existing.created_by
46428
+ created_by: existing.created_by,
46429
+ completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
46334
46430
  };
46335
46431
  await store.upsert("tasks", task2);
46336
46432
  return task2;
@@ -46338,11 +46434,11 @@ async function updateTask2(id, input, store) {
46338
46434
  async function startTask2(id, agentId, store) {
46339
46435
  const task2 = await requireRecord("tasks", id, store);
46340
46436
  if (task2.status !== "pending" && task2.status !== "in_progress") {
46341
- throw new Error(`Task is ${task2.status} and cannot be started by ${agentId}`);
46437
+ throw new TaskNotStartableError(task2.id, task2.status, agentId);
46342
46438
  }
46343
46439
  const started = await patchTask(task2, {
46344
46440
  status: "in_progress",
46345
- assigned_to: task2.assigned_to ?? agentId,
46441
+ assigned_to: task2.assigned_to || agentId,
46346
46442
  agent_id: task2.agent_id ?? agentId,
46347
46443
  locked_by: agentId,
46348
46444
  locked_at: new Date().toISOString(),
@@ -46353,8 +46449,13 @@ async function startTask2(id, agentId, store) {
46353
46449
  }
46354
46450
  async function completeTask2(id, agentId, options, store) {
46355
46451
  const task2 = await store.completeTask(id, agentId, options);
46356
- if (!task2)
46452
+ if (!task2) {
46453
+ const current = await store.get("tasks", id);
46454
+ if (current?.locked_by && !cloudLockExpired(current.locked_at) && !sameCloudLockHolder(current.locked_by, agentId)) {
46455
+ throw new LockError(id, current.locked_by);
46456
+ }
46357
46457
  throw new Error(`tasks record not found: ${id}`);
46458
+ }
46358
46459
  return task2;
46359
46460
  }
46360
46461
  async function failTask2(id, agentId, reason, options, store) {
@@ -46395,6 +46496,11 @@ async function patchTask(task2, patch, store) {
46395
46496
  await store.upsert("tasks", updated);
46396
46497
  return updated;
46397
46498
  }
46499
+ function sameCloudLockHolder(stored, incoming) {
46500
+ if (!stored || !incoming)
46501
+ return false;
46502
+ return canonicalAgentRef(stored) === canonicalAgentRef(incoming);
46503
+ }
46398
46504
  function cloudLockExpired(lockedAt) {
46399
46505
  if (!lockedAt)
46400
46506
  return true;
@@ -46712,6 +46818,16 @@ async function resolveAgent3(idOrName, store) {
46712
46818
  return byId;
46713
46819
  return matchAgentByName(await store.list("agents"), idOrName);
46714
46820
  }
46821
+ async function resolveAgentForAssignedFilter(idOrName, store) {
46822
+ const byId = await store.get("agents", idOrName);
46823
+ if (byId)
46824
+ return byId;
46825
+ const target = normalizeAgentNameInput(idOrName);
46826
+ if (!target)
46827
+ return null;
46828
+ const matches = (await store.list("agents")).filter((agent) => normalizeAgentNameInput(agent.name) === target);
46829
+ return matches.length === 1 ? matches[0] : null;
46830
+ }
46715
46831
  async function heartbeatAgent(idOrName, store, context) {
46716
46832
  const agent = await resolveAgent3(idOrName, store);
46717
46833
  if (!agent)
@@ -47027,6 +47143,7 @@ function postgresConstraintName(error) {
47027
47143
  var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC", TASK_ORDER_BY;
47028
47144
  var init_postgres_adapter = __esm(() => {
47029
47145
  init_types();
47146
+ init_creator_identity();
47030
47147
  init_postgres_sync();
47031
47148
  init_integrity();
47032
47149
  init_redaction();
@@ -48048,7 +48165,10 @@ function mapTaskError(e, json2) {
48048
48165
  retry_after: e.retryAfterSeconds ?? null
48049
48166
  }, 409);
48050
48167
  }
48051
- if (e instanceof Error && (/ is blocked by /.test(e.message) || /cannot be started/.test(e.message))) {
48168
+ if (e instanceof TaskNotStartableError) {
48169
+ return json2({ error: e.message, code: TaskNotStartableError.code }, 409);
48170
+ }
48171
+ if (e instanceof Error && / is blocked by /.test(e.message)) {
48052
48172
  return json2({ error: e.message, code: "TASK_NOT_STARTABLE" }, 409);
48053
48173
  }
48054
48174
  return null;
@@ -51682,6 +51802,9 @@ async function handleV1Request(req, url, dependencies = {}) {
51682
51802
  }
51683
51803
  if (e instanceof LockError)
51684
51804
  return error(409, e.message, { code: LockError.code });
51805
+ if (e instanceof TaskNotStartableError) {
51806
+ return error(409, e.message, { code: TaskNotStartableError.code });
51807
+ }
51685
51808
  if (e instanceof ResourceConflictError)
51686
51809
  return error(409, e.message, { code: e.code, conflict: true });
51687
51810
  if (e instanceof ProjectNotFoundError)
@@ -52508,6 +52631,8 @@ var init_serve = __esm(() => {
52508
52631
  // src/mcp/index.ts
52509
52632
  var exports_mcp = {};
52510
52633
  __export(exports_mcp, {
52634
+ formatTaskDetail: () => formatTaskDetail,
52635
+ formatTask: () => formatTask,
52511
52636
  buildServer: () => buildServer,
52512
52637
  applyFocus: () => applyFocus
52513
52638
  });
@@ -52656,7 +52781,8 @@ function resolveId(partialId, table = "tasks") {
52656
52781
  function formatTask(task2) {
52657
52782
  const id = task2.short_id || task2.id.slice(0, 8);
52658
52783
  const assigned = task2.assigned_to ? ` -> ${task2.assigned_to}` : "";
52659
- const lock = task2.locked_by ? ` [locked:${task2.locked_by}]` : "";
52784
+ const lockState = lockDisplayState(task2.locked_by, task2.locked_at);
52785
+ const lock = lockState.held ? ` [locked:${lockState.holder}]` : "";
52660
52786
  const recur = task2.recurrence_rule ? ` [\u21BB]` : "";
52661
52787
  return `${id} ${task2.status.padEnd(11)} ${task2.priority.padEnd(8)} ${task2.title}${assigned}${lock}${recur}`;
52662
52788
  }
@@ -52675,8 +52801,11 @@ function formatTaskDetail(task2, maxDescriptionChars) {
52675
52801
  parts.push(`Assigned to: ${task2.assigned_to}`);
52676
52802
  if (task2.agent_id)
52677
52803
  parts.push(`Agent: ${task2.agent_id}`);
52678
- if (task2.locked_by)
52679
- parts.push(`Locked by: ${task2.locked_by}`);
52804
+ const detailLock = lockDisplayState(task2.locked_by, task2.locked_at);
52805
+ if (detailLock.held)
52806
+ parts.push(`Locked by: ${detailLock.holder}`);
52807
+ else if (detailLock.expired)
52808
+ parts.push(`Lock: ${formatExpiredLock(detailLock)}`);
52680
52809
  if (task2.parent_id)
52681
52810
  parts.push(`Parent: ${task2.parent_id}`);
52682
52811
  if (task2.project_id)
@@ -52760,6 +52889,7 @@ var agentFocusMap, isDirectRun;
52760
52889
  var init_mcp2 = __esm(() => {
52761
52890
  init_agents();
52762
52891
  init_database();
52892
+ init_lock_display();
52763
52893
  init_types();
52764
52894
  init_dispatch2();
52765
52895
  init_task_crud2();
@@ -52802,6 +52932,8 @@ var init_mcp2 = __esm(() => {
52802
52932
  init_mcp2();
52803
52933
 
52804
52934
  export {
52935
+ formatTaskDetail,
52936
+ formatTask,
52805
52937
  buildServer,
52806
52938
  applyFocus
52807
52939
  };
package/dist/mcp.js CHANGED
@@ -41,7 +41,7 @@ var __require = import.meta.require;
41
41
  // package.json
42
42
  var package_default = {
43
43
  name: "@hasna/todos",
44
- version: "0.13.9",
44
+ version: "0.13.11",
45
45
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
46
46
  type: "module",
47
47
  main: "dist/index.js",
@@ -91,8 +91,8 @@ var package_default = {
91
91
  "README.md"
92
92
  ],
93
93
  scripts: {
94
- build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
95
- "build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
94
+ build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
95
+ "build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
96
96
  migrate: "bun run src/server/index.ts migrate",
97
97
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
98
98
  "generate:sdk": "bun run scripts/generate-sdk.ts",
package/dist/registry.js CHANGED
@@ -3430,7 +3430,7 @@ var init_machines = __esm(() => {
3430
3430
  function isBlockingDependencyStatus(status) {
3431
3431
  return status !== "completed" && status !== "cancelled";
3432
3432
  }
3433
- var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3433
+ var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3434
3434
  var init_types = __esm(() => {
3435
3435
  TASK_STATUSES = [
3436
3436
  "pending",
@@ -3470,6 +3470,20 @@ var init_types = __esm(() => {
3470
3470
  this.name = "TaskNotFoundError";
3471
3471
  }
3472
3472
  };
3473
+ TaskNotStartableError = class TaskNotStartableError extends Error {
3474
+ taskId;
3475
+ status;
3476
+ agentId;
3477
+ static code = "TASK_NOT_STARTABLE";
3478
+ static suggestion = "Reset the task status to pending before starting it again.";
3479
+ constructor(taskId, status, agentId) {
3480
+ super(`Task ${taskId} is ${status} and cannot be started by ${agentId}; ` + "reset the task status to pending before starting it again");
3481
+ this.taskId = taskId;
3482
+ this.status = status;
3483
+ this.agentId = agentId;
3484
+ this.name = "TaskNotStartableError";
3485
+ }
3486
+ };
3473
3487
  TaskReferenceAmbiguousError = class TaskReferenceAmbiguousError extends Error {
3474
3488
  reference;
3475
3489
  static code = "TASK_REFERENCE_AMBIGUOUS";
@@ -8242,7 +8256,7 @@ function assertStartable(task, agentId) {
8242
8256
  return;
8243
8257
  if (task.status === "in_progress")
8244
8258
  return;
8245
- throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
8259
+ throw new TaskNotStartableError(task.id, task.status, agentId);
8246
8260
  }
8247
8261
  function getBlockingDeps(id, db) {
8248
8262
  const d = db || getDatabase();
@@ -8313,7 +8327,7 @@ function completeTask(id, agentId, db, options) {
8313
8327
  if (task.status === "cancelled") {
8314
8328
  throw new Error(`Task ${id} is cancelled and cannot be completed`);
8315
8329
  }
8316
- if (agentId && task.locked_by && !sameHolder(task.locked_by, agentId) && !isLockExpired(task.locked_at)) {
8330
+ if (task.locked_by && !sameHolder(task.locked_by, agentId) && !isLockExpired(task.locked_at)) {
8317
8331
  throw new LockError(id, task.locked_by);
8318
8332
  }
8319
8333
  checkCompletionGuard(task, agentId || null, d);
@@ -8948,6 +8962,30 @@ function getTaskWithRelations(id, db) {
8948
8962
  checklist
8949
8963
  };
8950
8964
  }
8965
+ function resolveAssignedToAliases(db, ref) {
8966
+ const aliases = new Set([ref]);
8967
+ let agentId;
8968
+ try {
8969
+ agentId = resolvePartialId(db, "agents", ref);
8970
+ } catch (err) {
8971
+ if (!(err instanceof IdentityAliasAmbiguousError))
8972
+ throw err;
8973
+ agentId = null;
8974
+ }
8975
+ if (agentId) {
8976
+ aliases.add(agentId);
8977
+ const row = db.query("SELECT name FROM agents WHERE id = ?").get(agentId);
8978
+ if (row?.name)
8979
+ aliases.add(row.name);
8980
+ }
8981
+ return [...aliases];
8982
+ }
8983
+ function lowerInClause(column, values, params) {
8984
+ if (values.length === 0)
8985
+ return "1=0";
8986
+ params.push(...values.map((v) => v.toLowerCase()));
8987
+ return `LOWER(${column}) IN (${values.map(() => "?").join(",")})`;
8988
+ }
8951
8989
  function listTasks(filter = {}, db) {
8952
8990
  const d = db || getDatabase();
8953
8991
  const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
@@ -8989,8 +9027,7 @@ function listTasks(filter = {}, db) {
8989
9027
  }
8990
9028
  }
8991
9029
  if (filter.assigned_to) {
8992
- conditions.push("assigned_to = ?");
8993
- params.push(filter.assigned_to);
9030
+ conditions.push(lowerInClause("assigned_to", resolveAssignedToAliases(d, filter.assigned_to), params));
8994
9031
  }
8995
9032
  if (filter.agent_id) {
8996
9033
  conditions.push("agent_id = ?");
@@ -9150,8 +9187,7 @@ function countTasks(filter = {}, db) {
9150
9187
  }
9151
9188
  }
9152
9189
  if (filter.assigned_to) {
9153
- conditions.push("assigned_to = ?");
9154
- params.push(filter.assigned_to);
9190
+ conditions.push(lowerInClause("assigned_to", resolveAssignedToAliases(d, filter.assigned_to), params));
9155
9191
  }
9156
9192
  if (filter.agent_id) {
9157
9193
  conditions.push("agent_id = ?");
@@ -11973,7 +12009,7 @@ var init_tasks = __esm(() => {
11973
12009
  // package.json
11974
12010
  var package_default = {
11975
12011
  name: "@hasna/todos",
11976
- version: "0.13.9",
12012
+ version: "0.13.11",
11977
12013
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
11978
12014
  type: "module",
11979
12015
  main: "dist/index.js",
@@ -12023,8 +12059,8 @@ var package_default = {
12023
12059
  "README.md"
12024
12060
  ],
12025
12061
  scripts: {
12026
- build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
12027
- "build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
12062
+ build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
12063
+ "build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
12028
12064
  migrate: "bun run src/server/index.ts migrate",
12029
12065
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
12030
12066
  "generate:sdk": "bun run scripts/generate-sdk.ts",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "packageName": "@hasna/todos",
3
- "packageVersion": "0.13.9",
3
+ "packageVersion": "0.13.11",
4
4
  "repository": "https://github.com/hasna/todos.git",
5
- "gitCommit": "0877f637ea98eb762347fc78d43d033cf4999ef6",
6
- "gitTree": "458f99b7674b48d37f779b7bc1329369385b6ce5",
7
- "sourceTreeSha256": "49de4d648f9049fe5fdab5edfb8d47e17b5c5241b655c203f17fe0046b02e07f",
8
- "generatedAt": "2026-08-01T10:18:22.000Z"
5
+ "gitCommit": "af551bd57da4ef1835d763232c1e3b3b8cee4f9f",
6
+ "gitTree": "23029d1f9a1afeecf519674e13746e615431ae8b",
7
+ "sourceTreeSha256": "b14e57ef9ec0cbd0e0142681b993aa34c941d59a18649ddd499f3ee1a62cfef9",
8
+ "generatedAt": "2026-08-02T06:21:53.000Z"
9
9
  }