@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/cli/index.js CHANGED
@@ -2123,7 +2123,7 @@ var package_default;
2123
2123
  var init_package = __esm(() => {
2124
2124
  package_default = {
2125
2125
  name: "@hasna/todos",
2126
- version: "0.13.9",
2126
+ version: "0.13.11",
2127
2127
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
2128
2128
  type: "module",
2129
2129
  main: "dist/index.js",
@@ -2173,8 +2173,8 @@ var init_package = __esm(() => {
2173
2173
  "README.md"
2174
2174
  ],
2175
2175
  scripts: {
2176
- 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",
2177
- "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/*'",
2176
+ 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",
2177
+ "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/*'",
2178
2178
  migrate: "bun run src/server/index.ts migrate",
2179
2179
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
2180
2180
  "generate:sdk": "bun run scripts/generate-sdk.ts",
@@ -2258,7 +2258,7 @@ var init_package_version = __esm(() => {
2258
2258
  function isBlockingDependencyStatus(status) {
2259
2259
  return status !== "completed" && status !== "cancelled";
2260
2260
  }
2261
- var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
2261
+ var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
2262
2262
  var init_types = __esm(() => {
2263
2263
  TASK_STATUSES = [
2264
2264
  "pending",
@@ -2297,6 +2297,20 @@ var init_types = __esm(() => {
2297
2297
  this.name = "TaskNotFoundError";
2298
2298
  }
2299
2299
  };
2300
+ TaskNotStartableError = class TaskNotStartableError extends Error {
2301
+ taskId;
2302
+ status;
2303
+ agentId;
2304
+ static code = "TASK_NOT_STARTABLE";
2305
+ static suggestion = "Reset the task status to pending before starting it again.";
2306
+ constructor(taskId, status, agentId) {
2307
+ super(`Task ${taskId} is ${status} and cannot be started by ${agentId}; ` + "reset the task status to pending before starting it again");
2308
+ this.taskId = taskId;
2309
+ this.status = status;
2310
+ this.agentId = agentId;
2311
+ this.name = "TaskNotStartableError";
2312
+ }
2313
+ };
2300
2314
  TaskReferenceAmbiguousError = class TaskReferenceAmbiguousError extends Error {
2301
2315
  reference;
2302
2316
  static code = "TASK_REFERENCE_AMBIGUOUS";
@@ -5174,6 +5188,16 @@ function classifyRemoteRequestError(baseUrl, route, error) {
5174
5188
  if (status === 403) {
5175
5189
  throw new Error(`REMOTE_API_FORBIDDEN: configured Todos authority ${baseUrl} denied ${route}; local SQLite fallback is disabled`, { cause: error });
5176
5190
  }
5191
+ if (status === 409) {
5192
+ const body = error && typeof error === "object" ? error.body : undefined;
5193
+ if (body && typeof body === "object" && !Array.isArray(body)) {
5194
+ const remoteError = body;
5195
+ if (remoteError.code === "TASK_NOT_STARTABLE" && typeof remoteError.error === "string" && remoteError.error.length > 0) {
5196
+ throw new Error(`${remoteError.code}: ${remoteError.error}`, { cause: error });
5197
+ }
5198
+ }
5199
+ throw error;
5200
+ }
5177
5201
  if (typeof status === "number" && status >= 300 && status < 400) {
5178
5202
  throw new Error(`REMOTE_API_REDIRECT_REJECTED: configured Todos authority ${baseUrl} redirected ${route}; ` + "authenticated redirects are disabled to prevent credential leakage", { cause: error });
5179
5203
  }
@@ -5608,7 +5632,10 @@ function resolveCloudProjectRef(projects, ref) {
5608
5632
  if (matches.length > 1)
5609
5633
  throw new Error(`Project reference is ambiguous: "${input}"`);
5610
5634
  }
5611
- throw new Error(`Project not found: "${input}"`);
5635
+ if (UUID_RE.test(input) || /^[0-9a-f]{4,}$/i.test(input)) {
5636
+ throw new Error(`Project not found: "${input}"`);
5637
+ }
5638
+ 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.`);
5612
5639
  }
5613
5640
  async function cloudResolveProjectRef(client, ref) {
5614
5641
  return resolveCloudProjectRef(await cloudListProjects(client), ref);
@@ -5625,6 +5652,16 @@ async function cloudUpdateProject(client, id, patch) {
5625
5652
  }
5626
5653
  return raw;
5627
5654
  }
5655
+ async function cloudDeleteProject(client, id) {
5656
+ try {
5657
+ await client.transport.del(`/projects/${encodeURIComponent(id)}`);
5658
+ } catch (error) {
5659
+ if (error && typeof error === "object" && error.status === 404)
5660
+ return false;
5661
+ throw error;
5662
+ }
5663
+ return true;
5664
+ }
5628
5665
  async function cloudListPlans(client, projectId) {
5629
5666
  const query = projectId ? { project_id: projectId } : {};
5630
5667
  const res = await requiredRemoteRoute(client, "/v1/plans", () => client.list("plans", { query }));
@@ -6412,7 +6449,9 @@ function disqualifyingArgument(invocation) {
6412
6449
  return dropIt("the `routing` subcommand");
6413
6450
  return firstPresentOption(args, ["--apply", "--fix"]);
6414
6451
  case "projects":
6415
- return firstPresentOption(args, ["--deregister", "--path-prefix", "--dry-run"]);
6452
+ if (hasOption(args, "--deregister"))
6453
+ return null;
6454
+ return firstPresentOption(args, ["--path-prefix", "--dry-run"]);
6416
6455
  case "plans":
6417
6456
  return firstPresentOption(args, ["--artifact", "--write-artifacts"]);
6418
6457
  case "list":
@@ -11363,6 +11402,24 @@ var init_projects = __esm(() => {
11363
11402
  init_storage_tombstones();
11364
11403
  });
11365
11404
 
11405
+ // src/lib/lock-display.ts
11406
+ function lockDisplayState(lockedBy, lockedAt, nowMs = Date.now()) {
11407
+ const holder = lockedBy ?? null;
11408
+ const at = lockedAt ?? null;
11409
+ if (!holder) {
11410
+ return { present: false, held: false, expired: false, holder: null, lockedAt: at };
11411
+ }
11412
+ const expired = isLockExpired(at, nowMs);
11413
+ return { present: true, held: !expired, expired, holder, lockedAt: at };
11414
+ }
11415
+ function formatExpiredLock(state) {
11416
+ const at = state.lockedAt ? ` at ${state.lockedAt}` : "";
11417
+ return `expired (last held by ${state.holder}${at})`;
11418
+ }
11419
+ var init_lock_display = __esm(() => {
11420
+ init_database();
11421
+ });
11422
+
11366
11423
  // src/cli/helpers.ts
11367
11424
  var exports_helpers = {};
11368
11425
  __export(exports_helpers, {
@@ -11628,7 +11685,8 @@ function output(data, jsonMode) {
11628
11685
  function formatTaskLine(t) {
11629
11686
  const statusFn = statusColors[t.status] || chalk.white;
11630
11687
  const priorityFn = priorityColors[t.priority] || chalk.white;
11631
- const lock = t.locked_by ? chalk.magenta(` [locked:${t.locked_by}]`) : "";
11688
+ const lockState = lockDisplayState(t.locked_by, t.locked_at);
11689
+ const lock = lockState.held ? chalk.magenta(` [locked:${lockState.holder}]`) : "";
11632
11690
  const assigned = t.assigned_to ? chalk.cyan(` -> ${t.assigned_to}`) : "";
11633
11691
  const tags = t.tags.length > 0 ? chalk.dim(` [${t.tags.join(",")}]`) : "";
11634
11692
  const plan = t.plan_id ? chalk.magenta(` [plan:${t.plan_id.slice(0, 8)}]`) : "";
@@ -11639,6 +11697,7 @@ var init_helpers = __esm(() => {
11639
11697
  init_cloud_router();
11640
11698
  init_database();
11641
11699
  init_projects();
11700
+ init_lock_display();
11642
11701
  init_package_version();
11643
11702
  stdoutRetryBuffer = new SharedArrayBuffer(4);
11644
11703
  stdoutRetrySignal = new Int32Array(stdoutRetryBuffer);
@@ -14865,7 +14924,7 @@ function assertStartable(task, agentId) {
14865
14924
  return;
14866
14925
  if (task.status === "in_progress")
14867
14926
  return;
14868
- throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
14927
+ throw new TaskNotStartableError(task.id, task.status, agentId);
14869
14928
  }
14870
14929
  function getBlockingDeps(id, db) {
14871
14930
  const d = db || getDatabase();
@@ -14936,7 +14995,7 @@ function completeTask(id, agentId, db, options) {
14936
14995
  if (task.status === "cancelled") {
14937
14996
  throw new Error(`Task ${id} is cancelled and cannot be completed`);
14938
14997
  }
14939
- if (agentId && task.locked_by && !sameHolder(task.locked_by, agentId) && !isLockExpired(task.locked_at)) {
14998
+ if (task.locked_by && !sameHolder(task.locked_by, agentId) && !isLockExpired(task.locked_at)) {
14940
14999
  throw new LockError(id, task.locked_by);
14941
15000
  }
14942
15001
  checkCompletionGuard(task, agentId || null, d);
@@ -15571,6 +15630,30 @@ function getTaskWithRelations(id, db) {
15571
15630
  checklist
15572
15631
  };
15573
15632
  }
15633
+ function resolveAssignedToAliases(db, ref) {
15634
+ const aliases = new Set([ref]);
15635
+ let agentId;
15636
+ try {
15637
+ agentId = resolvePartialId(db, "agents", ref);
15638
+ } catch (err) {
15639
+ if (!(err instanceof IdentityAliasAmbiguousError))
15640
+ throw err;
15641
+ agentId = null;
15642
+ }
15643
+ if (agentId) {
15644
+ aliases.add(agentId);
15645
+ const row = db.query("SELECT name FROM agents WHERE id = ?").get(agentId);
15646
+ if (row?.name)
15647
+ aliases.add(row.name);
15648
+ }
15649
+ return [...aliases];
15650
+ }
15651
+ function lowerInClause(column, values, params) {
15652
+ if (values.length === 0)
15653
+ return "1=0";
15654
+ params.push(...values.map((v) => v.toLowerCase()));
15655
+ return `LOWER(${column}) IN (${values.map(() => "?").join(",")})`;
15656
+ }
15574
15657
  function listTasks(filter = {}, db) {
15575
15658
  const d = db || getDatabase();
15576
15659
  const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
@@ -15612,8 +15695,7 @@ function listTasks(filter = {}, db) {
15612
15695
  }
15613
15696
  }
15614
15697
  if (filter.assigned_to) {
15615
- conditions.push("assigned_to = ?");
15616
- params.push(filter.assigned_to);
15698
+ conditions.push(lowerInClause("assigned_to", resolveAssignedToAliases(d, filter.assigned_to), params));
15617
15699
  }
15618
15700
  if (filter.agent_id) {
15619
15701
  conditions.push("agent_id = ?");
@@ -15773,8 +15855,7 @@ function countTasks(filter = {}, db) {
15773
15855
  }
15774
15856
  }
15775
15857
  if (filter.assigned_to) {
15776
- conditions.push("assigned_to = ?");
15777
- params.push(filter.assigned_to);
15858
+ conditions.push(lowerInClause("assigned_to", resolveAssignedToAliases(d, filter.assigned_to), params));
15778
15859
  }
15779
15860
  if (filter.agent_id) {
15780
15861
  conditions.push("agent_id = ?");
@@ -19009,10 +19090,11 @@ function validateAssignee(input, ctx) {
19009
19090
  if (ctx.allowSeat) {
19010
19091
  return { ok: true, assignee: byId ? byId.name : raw, agentId: byId?.id, isSeat: true };
19011
19092
  }
19093
+ const hint = ctx.seatHint?.(raw);
19012
19094
  return {
19013
19095
  ok: false,
19014
19096
  reason: "seat",
19015
- 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.`
19097
+ 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).")
19016
19098
  };
19017
19099
  }
19018
19100
  if (byId) {
@@ -19740,10 +19822,10 @@ var init_agents = __esm(() => {
19740
19822
 
19741
19823
  // src/cli/assignee-guard.ts
19742
19824
  import chalk2 from "chalk";
19743
- async function resolveValidatedAssignee(value, allowSeat) {
19825
+ async function resolveValidatedAssignee(value, allowSeat, buildSeatHint) {
19744
19826
  const cloud = getTodosCloudClient();
19745
19827
  const ctx = await loadAssigneeContext(() => cloud ? cloudListAgents(cloud) : listAgents(), allowSeat);
19746
- const verdict = validateAssignee(value, ctx);
19828
+ const verdict = validateAssignee(value, { ...ctx, seatHint: buildSeatHint });
19747
19829
  if (!verdict.ok) {
19748
19830
  handleError(new Error(`Cannot assign to '${value}'. ${verdict.message}`));
19749
19831
  }
@@ -20263,7 +20345,7 @@ function registerTaskCommands(program2) {
20263
20345
  opts.list = opts.list || opts.taskList;
20264
20346
  const creator = resolveCreatorIdentity(opts.createdBy || globalOpts.agent);
20265
20347
  const router = resolveWritableIdentity(opts.createdBy || globalOpts.agent);
20266
- const requestedAssign = opts.assign ? await resolveValidatedAssignee(opts.assign, Boolean(opts.assignSeat)) : undefined;
20348
+ const requestedAssign = opts.assign ? await resolveValidatedAssignee(opts.assign, Boolean(opts.assignSeat), (v) => `--assign ${v} --assign-seat`) : undefined;
20267
20349
  const assignee = requestedAssign || (opts.unassigned ? undefined : router.agent_id || undefined);
20268
20350
  if (!assignee && !opts.unassigned) {
20269
20351
  console.error(chalk3.yellow("Warning: task is ownerless and unattributable \u2014 export TODOS_AGENT_ID=<name> for this session, or pass --agent/--assign <agent> or --unassigned."));
@@ -20366,7 +20448,7 @@ function registerTaskCommands(program2) {
20366
20448
  opts.tags = opts.tags || opts.tag;
20367
20449
  opts.list = opts.list || opts.taskList;
20368
20450
  if (opts.assign) {
20369
- opts.assign = await resolveValidatedAssignee(opts.assign, Boolean(opts.assignSeat));
20451
+ opts.assign = await resolveValidatedAssignee(opts.assign, Boolean(opts.assignSeat), (v) => `--assign ${v} --assign-seat`);
20370
20452
  }
20371
20453
  const explicitProject = opts.project || globalOpts.project;
20372
20454
  const cloud = getTodosCloudClient();
@@ -20736,8 +20818,11 @@ function registerTaskCommands(program2) {
20736
20818
  console.log(` ${chalk3.dim("Agent:")} ${task2.agent_id}`);
20737
20819
  if (task2.session_id)
20738
20820
  console.log(` ${chalk3.dim("Session:")} ${task2.session_id}`);
20739
- if (task2.locked_by)
20740
- console.log(` ${chalk3.dim("Locked:")} ${task2.locked_by} (at ${task2.locked_at})`);
20821
+ const showLock = lockDisplayState(task2.locked_by, task2.locked_at);
20822
+ if (showLock.held)
20823
+ console.log(` ${chalk3.dim("Locked:")} ${showLock.holder} (at ${showLock.lockedAt})`);
20824
+ else if (showLock.expired)
20825
+ console.log(` ${chalk3.dim("Lock:")} ${chalk3.dim(formatExpiredLock(showLock))}`);
20741
20826
  if (task2.requires_approval) {
20742
20827
  const approvalStatus = task2.approved_by ? chalk3.green(`approved by ${task2.approved_by}`) : chalk3.yellow("pending approval");
20743
20828
  console.log(` ${chalk3.dim("Approval:")} ${approvalStatus}`);
@@ -20880,8 +20965,11 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
20880
20965
  }
20881
20966
  if (task2.assigned_to)
20882
20967
  console.log(` ${chalk3.dim("Assigned:")} ${task2.assigned_to}`);
20883
- if (task2.locked_by)
20884
- console.log(` ${chalk3.dim("Locked by:")} ${task2.locked_by}`);
20968
+ const inspectLock = lockDisplayState(task2.locked_by, task2.locked_at);
20969
+ if (inspectLock.held)
20970
+ console.log(` ${chalk3.dim("Locked by:")} ${inspectLock.holder}`);
20971
+ else if (inspectLock.expired)
20972
+ console.log(` ${chalk3.dim("Lock:")} ${chalk3.dim(formatExpiredLock(inspectLock))}`);
20885
20973
  if (task2.project_id)
20886
20974
  console.log(` ${chalk3.dim("Project:")} ${task2.project_id}`);
20887
20975
  if (task2.plan_id)
@@ -21021,7 +21109,7 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21021
21109
  handleError(new Error("Use either --working-dir or --clear-working-dir, not both."));
21022
21110
  }
21023
21111
  if (opts.assign) {
21024
- opts.assign = await resolveValidatedAssignee(opts.assign, Boolean(opts.assignSeat));
21112
+ opts.assign = await resolveValidatedAssignee(opts.assign, Boolean(opts.assignSeat), (v) => `--assign ${v} --assign-seat`);
21025
21113
  }
21026
21114
  const cloud = getTodosCloudClient();
21027
21115
  if (cloud) {
@@ -21189,10 +21277,12 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21189
21277
  };
21190
21278
  const cloud = getTodosCloudClient();
21191
21279
  if (cloud) {
21280
+ const resolvedId2 = await resolveTaskIdForCommand(id, cloud);
21281
+ const agentId2 = resolveClaimIdentity("complete", globalOpts.agent);
21192
21282
  let task3;
21193
21283
  try {
21194
- task3 = await cloudCompleteTask(cloud, await resolveTaskIdForCommand(id, cloud), {
21195
- ...globalOpts.agent ? { agent_id: globalOpts.agent } : {},
21284
+ task3 = await cloudCompleteTask(cloud, resolvedId2, {
21285
+ agent_id: agentId2,
21196
21286
  ...completionOptions
21197
21287
  });
21198
21288
  } catch (e) {
@@ -21207,9 +21297,10 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21207
21297
  return;
21208
21298
  }
21209
21299
  const resolvedId = resolveTaskId(id);
21300
+ const agentId = resolveClaimIdentity("complete", globalOpts.agent);
21210
21301
  let task2;
21211
21302
  try {
21212
- task2 = completeTask(resolvedId, globalOpts.agent, undefined, completionOptions);
21303
+ task2 = completeTask(resolvedId, agentId, undefined, completionOptions);
21213
21304
  } catch (e) {
21214
21305
  handleError(e);
21215
21306
  }
@@ -21492,6 +21583,7 @@ var init_task_commands = __esm(() => {
21492
21583
  init_task_lists();
21493
21584
  init_cloud_router();
21494
21585
  init_creator_identity();
21586
+ init_lock_display();
21495
21587
  init_claim_guard();
21496
21588
  init_assignee_guard();
21497
21589
  init_helpers();
@@ -26176,6 +26268,12 @@ function countProjectTasks(projectId) {
26176
26268
  FROM tasks
26177
26269
  WHERE project_id = ?`).get(projectId);
26178
26270
  }
26271
+ function countTasksForDeregistration(tasks) {
26272
+ return {
26273
+ total: tasks.length,
26274
+ incomplete: tasks.filter((task) => task.status !== "completed" && task.status !== "cancelled").length
26275
+ };
26276
+ }
26179
26277
  function pathIsWithinPrefix(projectPath, prefix) {
26180
26278
  const normalizedPath = resolve13(projectPath);
26181
26279
  const normalizedPrefix = resolve13(prefix);
@@ -26651,11 +26749,8 @@ function registerProjectCommands(program2) {
26651
26749
  return;
26652
26750
  }
26653
26751
  if (opts.deregister) {
26654
- if (cloud) {
26655
- handleError(new Error("REMOTE_COMMAND_UNSUPPORTED: projects --deregister has no safe /v1 equivalent; local SQLite fallback is disabled"));
26656
- }
26657
- const project = resolveExplicitProject(opts.deregister);
26658
- const counts = countProjectTasks(project.id);
26752
+ const project = cloud ? await cloudResolveProject(cloud, opts.deregister) : resolveExplicitProject(opts.deregister);
26753
+ const counts = cloud ? countTasksForDeregistration(await cloudListTasks(cloud, { project_id: project.id, include_subtasks: true })) : countProjectTasks(project.id);
26659
26754
  if (opts.pathPrefix && !pathIsWithinPrefix(project.path, opts.pathPrefix)) {
26660
26755
  handleError(new Error(`Refusing to deregister ${project.name}: path ${project.path} is not within ${opts.pathPrefix}`));
26661
26756
  }
@@ -26672,7 +26767,10 @@ function registerProjectCommands(program2) {
26672
26767
  tasks_preserved: true
26673
26768
  };
26674
26769
  if (!opts.dryRun) {
26675
- deleteProject(project.id);
26770
+ if (cloud)
26771
+ await cloudDeleteProject(cloud, project.id);
26772
+ else
26773
+ deleteProject(project.id);
26676
26774
  }
26677
26775
  if (globalOpts.json) {
26678
26776
  output(result, true);
@@ -31438,7 +31536,16 @@ class PostgresJsonRecordStore {
31438
31536
  updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString()
31439
31537
  }));
31440
31538
  }
31441
- buildTaskFilterSql(filter) {
31539
+ async resolveAssignedToAliases(ref) {
31540
+ const agent = await resolveAgentForAssignedFilter(ref, this);
31541
+ const aliases = new Set([ref]);
31542
+ if (agent) {
31543
+ aliases.add(agent.id);
31544
+ aliases.add(agent.name);
31545
+ }
31546
+ return [...aliases];
31547
+ }
31548
+ async buildTaskFilterSql(filter) {
31442
31549
  const params = [this.service, "tasks"];
31443
31550
  const conds = ["service = $1", "object_type = $2", "deleted_at IS NULL"];
31444
31551
  const p = (value) => {
@@ -31464,8 +31571,10 @@ class PostgresJsonRecordStore {
31464
31571
  conds.push(inClause("payload->>'status'", toFilterArray(filter.status)));
31465
31572
  if (filter.priority !== undefined)
31466
31573
  conds.push(inClause("payload->>'priority'", toFilterArray(filter.priority)));
31467
- if (filter.assigned_to !== undefined)
31468
- conds.push(`payload->>'assigned_to' = ${p(filter.assigned_to)}`);
31574
+ if (filter.assigned_to !== undefined) {
31575
+ const aliases = [...new Set((await this.resolveAssignedToAliases(filter.assigned_to)).map((a) => a.toLowerCase()))];
31576
+ conds.push(inClause("LOWER(payload->>'assigned_to')", aliases));
31577
+ }
31469
31578
  if (filter.agent_id !== undefined)
31470
31579
  conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
31471
31580
  if (filter.created_by !== undefined)
@@ -31499,7 +31608,7 @@ class PostgresJsonRecordStore {
31499
31608
  }
31500
31609
  async listTasks(filter) {
31501
31610
  await this.ensureSchema();
31502
- const { where, params, queryRef } = this.buildTaskFilterSql(filter);
31611
+ const { where, params, queryRef } = await this.buildTaskFilterSql(filter);
31503
31612
  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;
31504
31613
  let sql = `/* todos:list-tasks */ SELECT payload FROM ${this.tableName} WHERE ${where} ${orderBy}`;
31505
31614
  if (filter.limit !== undefined) {
@@ -31559,7 +31668,7 @@ class PostgresJsonRecordStore {
31559
31668
  }
31560
31669
  async countTasks(filter) {
31561
31670
  await this.ensureSchema();
31562
- const { where, params } = this.buildTaskFilterSql(filter);
31671
+ const { where, params } = await this.buildTaskFilterSql(filter);
31563
31672
  const sql = `/* todos:count-tasks */ SELECT COUNT(*)::int AS count FROM ${this.tableName} WHERE ${where}`;
31564
31673
  const result = await this.options.client.query(sql, params);
31565
31674
  return Number(result.rows[0]?.count ?? 0);
@@ -31720,9 +31829,13 @@ class PostgresJsonRecordStore {
31720
31829
  } : {};
31721
31830
  const hasEvidence = Object.keys(evidence).length > 0;
31722
31831
  const hasConfidence = options?.confidence !== undefined;
31723
- const result = await this.options.client.query(`/* todos:complete-task-atomic */ UPDATE ${this.tableName}
31832
+ const lockExpiryCutoff2 = new Date(new Date(operationTimestamp).getTime() - CLOUD_LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
31833
+ const result = await this.options.client.query(`/* todos:complete-task-atomic todos:complete-task-lock-guard todos:complete-task-clears-lock */
31834
+ UPDATE ${this.tableName}
31724
31835
  SET payload = payload || jsonb_build_object(
31725
31836
  'status', 'completed',
31837
+ 'locked_by', 'null'::jsonb,
31838
+ 'locked_at', 'null'::jsonb,
31726
31839
  'assigned_to', CASE
31727
31840
  WHEN jsonb_typeof(payload->'assigned_to') = 'string' THEN payload->'assigned_to'
31728
31841
  ELSE COALESCE(to_jsonb($3::text), 'null'::jsonb)
@@ -31750,6 +31863,16 @@ class PostgresJsonRecordStore {
31750
31863
  updated_at = $9::timestamptz,
31751
31864
  version = COALESCE(version, 0) + 1
31752
31865
  WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL
31866
+ AND (
31867
+ payload->>'locked_by' IS NULL
31868
+ OR BTRIM(payload->>'locked_by') = ''
31869
+ OR payload->>'locked_at' IS NULL
31870
+ OR payload->>'locked_at' < $10::text
31871
+ OR (
31872
+ $3::text IS NOT NULL
31873
+ AND LOWER(BTRIM(payload->>'locked_by')) = LOWER(BTRIM($3::text))
31874
+ )
31875
+ )
31753
31876
  RETURNING payload`, [
31754
31877
  this.service,
31755
31878
  id,
@@ -31759,7 +31882,8 @@ class PostgresJsonRecordStore {
31759
31882
  jsonbParam(evidence),
31760
31883
  hasConfidence,
31761
31884
  options?.confidence ?? null,
31762
- operationTimestamp
31885
+ operationTimestamp,
31886
+ lockExpiryCutoff2
31763
31887
  ]);
31764
31888
  return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
31765
31889
  }
@@ -32040,6 +32164,7 @@ async function updateTask2(id, input, store) {
32040
32164
  if (existing.version !== input.version) {
32041
32165
  throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
32042
32166
  }
32167
+ const reopened = existing.status === "completed" && input.status !== undefined && input.status !== "completed" && input.completed_at === undefined;
32043
32168
  const task = {
32044
32169
  ...existing,
32045
32170
  ...definedPatch(input),
@@ -32049,7 +32174,8 @@ async function updateTask2(id, input, store) {
32049
32174
  metadata: input.metadata ?? existing.metadata,
32050
32175
  requires_approval: input.requires_approval ?? existing.requires_approval,
32051
32176
  task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
32052
- created_by: existing.created_by
32177
+ created_by: existing.created_by,
32178
+ completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
32053
32179
  };
32054
32180
  await store.upsert("tasks", task);
32055
32181
  return task;
@@ -32057,11 +32183,11 @@ async function updateTask2(id, input, store) {
32057
32183
  async function startTask2(id, agentId, store) {
32058
32184
  const task = await requireRecord("tasks", id, store);
32059
32185
  if (task.status !== "pending" && task.status !== "in_progress") {
32060
- throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
32186
+ throw new TaskNotStartableError(task.id, task.status, agentId);
32061
32187
  }
32062
32188
  const started = await patchTask(task, {
32063
32189
  status: "in_progress",
32064
- assigned_to: task.assigned_to ?? agentId,
32190
+ assigned_to: task.assigned_to || agentId,
32065
32191
  agent_id: task.agent_id ?? agentId,
32066
32192
  locked_by: agentId,
32067
32193
  locked_at: new Date().toISOString(),
@@ -32072,8 +32198,13 @@ async function startTask2(id, agentId, store) {
32072
32198
  }
32073
32199
  async function completeTask2(id, agentId, options, store) {
32074
32200
  const task = await store.completeTask(id, agentId, options);
32075
- if (!task)
32201
+ if (!task) {
32202
+ const current = await store.get("tasks", id);
32203
+ if (current?.locked_by && !cloudLockExpired(current.locked_at) && !sameCloudLockHolder(current.locked_by, agentId)) {
32204
+ throw new LockError(id, current.locked_by);
32205
+ }
32076
32206
  throw new Error(`tasks record not found: ${id}`);
32207
+ }
32077
32208
  return task;
32078
32209
  }
32079
32210
  async function failTask2(id, agentId, reason, options, store) {
@@ -32114,6 +32245,11 @@ async function patchTask(task, patch, store) {
32114
32245
  await store.upsert("tasks", updated);
32115
32246
  return updated;
32116
32247
  }
32248
+ function sameCloudLockHolder(stored, incoming) {
32249
+ if (!stored || !incoming)
32250
+ return false;
32251
+ return canonicalAgentRef(stored) === canonicalAgentRef(incoming);
32252
+ }
32117
32253
  function cloudLockExpired(lockedAt) {
32118
32254
  if (!lockedAt)
32119
32255
  return true;
@@ -32431,6 +32567,16 @@ async function resolveAgent(idOrName, store) {
32431
32567
  return byId;
32432
32568
  return matchAgentByName(await store.list("agents"), idOrName);
32433
32569
  }
32570
+ async function resolveAgentForAssignedFilter(idOrName, store) {
32571
+ const byId = await store.get("agents", idOrName);
32572
+ if (byId)
32573
+ return byId;
32574
+ const target = normalizeAgentNameInput(idOrName);
32575
+ if (!target)
32576
+ return null;
32577
+ const matches = (await store.list("agents")).filter((agent) => normalizeAgentNameInput(agent.name) === target);
32578
+ return matches.length === 1 ? matches[0] : null;
32579
+ }
32434
32580
  async function heartbeatAgent(idOrName, store, context) {
32435
32581
  const agent = await resolveAgent(idOrName, store);
32436
32582
  if (!agent)
@@ -32746,6 +32892,7 @@ function postgresConstraintName(error) {
32746
32892
  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;
32747
32893
  var init_postgres_adapter = __esm(() => {
32748
32894
  init_types();
32895
+ init_creator_identity();
32749
32896
  init_postgres_sync();
32750
32897
  init_integrity();
32751
32898
  init_redaction();
@@ -34235,7 +34382,10 @@ function mapTaskError(e, json2) {
34235
34382
  retry_after: e.retryAfterSeconds ?? null
34236
34383
  }, 409);
34237
34384
  }
34238
- if (e instanceof Error && (/ is blocked by /.test(e.message) || /cannot be started/.test(e.message))) {
34385
+ if (e instanceof TaskNotStartableError) {
34386
+ return json2({ error: e.message, code: TaskNotStartableError.code }, 409);
34387
+ }
34388
+ if (e instanceof Error && / is blocked by /.test(e.message)) {
34239
34389
  return json2({ error: e.message, code: "TASK_NOT_STARTABLE" }, 409);
34240
34390
  }
34241
34391
  return null;
@@ -38800,6 +38950,9 @@ async function handleV1Request(req, url, dependencies = {}) {
38800
38950
  }
38801
38951
  if (e instanceof LockError)
38802
38952
  return error(409, e.message, { code: LockError.code });
38953
+ if (e instanceof TaskNotStartableError) {
38954
+ return error(409, e.message, { code: TaskNotStartableError.code });
38955
+ }
38803
38956
  if (e instanceof ResourceConflictError)
38804
38957
  return error(409, e.message, { code: e.code, conflict: true });
38805
38958
  if (e instanceof ProjectNotFoundError)
@@ -62408,6 +62561,8 @@ var init_workflow_prompts2 = __esm(() => {
62408
62561
  // src/mcp/index.ts
62409
62562
  var exports_mcp = {};
62410
62563
  __export(exports_mcp, {
62564
+ formatTaskDetail: () => formatTaskDetail,
62565
+ formatTask: () => formatTask,
62411
62566
  buildServer: () => buildServer,
62412
62567
  applyFocus: () => applyFocus
62413
62568
  });
@@ -62556,7 +62711,8 @@ function resolveId(partialId, table = "tasks") {
62556
62711
  function formatTask(task2) {
62557
62712
  const id = task2.short_id || task2.id.slice(0, 8);
62558
62713
  const assigned = task2.assigned_to ? ` -> ${task2.assigned_to}` : "";
62559
- const lock = task2.locked_by ? ` [locked:${task2.locked_by}]` : "";
62714
+ const lockState = lockDisplayState(task2.locked_by, task2.locked_at);
62715
+ const lock = lockState.held ? ` [locked:${lockState.holder}]` : "";
62560
62716
  const recur = task2.recurrence_rule ? ` [\u21BB]` : "";
62561
62717
  return `${id} ${task2.status.padEnd(11)} ${task2.priority.padEnd(8)} ${task2.title}${assigned}${lock}${recur}`;
62562
62718
  }
@@ -62575,8 +62731,11 @@ function formatTaskDetail(task2, maxDescriptionChars) {
62575
62731
  parts.push(`Assigned to: ${task2.assigned_to}`);
62576
62732
  if (task2.agent_id)
62577
62733
  parts.push(`Agent: ${task2.agent_id}`);
62578
- if (task2.locked_by)
62579
- parts.push(`Locked by: ${task2.locked_by}`);
62734
+ const detailLock = lockDisplayState(task2.locked_by, task2.locked_at);
62735
+ if (detailLock.held)
62736
+ parts.push(`Locked by: ${detailLock.holder}`);
62737
+ else if (detailLock.expired)
62738
+ parts.push(`Lock: ${formatExpiredLock(detailLock)}`);
62580
62739
  if (task2.parent_id)
62581
62740
  parts.push(`Parent: ${task2.parent_id}`);
62582
62741
  if (task2.project_id)
@@ -62660,6 +62819,7 @@ var agentFocusMap, isDirectRun;
62660
62819
  var init_mcp2 = __esm(() => {
62661
62820
  init_agents();
62662
62821
  init_database();
62822
+ init_lock_display();
62663
62823
  init_types();
62664
62824
  init_dispatch2();
62665
62825
  init_task_crud2();
@@ -63705,7 +63865,7 @@ function TaskList({ tasks, selectedIndex }) {
63705
63865
  task2.assigned_to
63706
63866
  ]
63707
63867
  }, undefined, true, undefined, this),
63708
- task2.locked_by && /* @__PURE__ */ jsxDEV2(Text2, {
63868
+ lockDisplayState(task2.locked_by, task2.locked_at).held && /* @__PURE__ */ jsxDEV2(Text2, {
63709
63869
  color: "magenta",
63710
63870
  children: [
63711
63871
  " \uD83D\uDD12",
@@ -63727,6 +63887,7 @@ function TaskList({ tasks, selectedIndex }) {
63727
63887
  }
63728
63888
  var statusIcons, statusColors2, priorityLabels, priorityColors2;
63729
63889
  var init_TaskList = __esm(() => {
63890
+ init_lock_display();
63730
63891
  statusIcons = {
63731
63892
  pending: "\u25CB",
63732
63893
  in_progress: "\u25D0",
@@ -63889,7 +64050,7 @@ function TaskDetail({ task: task2 }) {
63889
64050
  }, undefined, false, undefined, this)
63890
64051
  ]
63891
64052
  }, undefined, true, undefined, this),
63892
- task2.locked_by && /* @__PURE__ */ jsxDEV3(Box3, {
64053
+ lockDisplayState(task2.locked_by, task2.locked_at).held && /* @__PURE__ */ jsxDEV3(Box3, {
63893
64054
  children: [
63894
64055
  /* @__PURE__ */ jsxDEV3(Text3, {
63895
64056
  dimColor: true,
@@ -63906,6 +64067,18 @@ function TaskDetail({ task: task2 }) {
63906
64067
  }, undefined, true, undefined, this)
63907
64068
  ]
63908
64069
  }, undefined, true, undefined, this),
64070
+ lockDisplayState(task2.locked_by, task2.locked_at).expired && /* @__PURE__ */ jsxDEV3(Box3, {
64071
+ children: [
64072
+ /* @__PURE__ */ jsxDEV3(Text3, {
64073
+ dimColor: true,
64074
+ children: "Lock: "
64075
+ }, undefined, false, undefined, this),
64076
+ /* @__PURE__ */ jsxDEV3(Text3, {
64077
+ dimColor: true,
64078
+ children: formatExpiredLock(lockDisplayState(task2.locked_by, task2.locked_at))
64079
+ }, undefined, false, undefined, this)
64080
+ ]
64081
+ }, undefined, true, undefined, this),
63909
64082
  task2.tags.length > 0 && /* @__PURE__ */ jsxDEV3(Box3, {
63910
64083
  children: [
63911
64084
  /* @__PURE__ */ jsxDEV3(Text3, {
@@ -64108,6 +64281,7 @@ function TaskDetail({ task: task2 }) {
64108
64281
  }
64109
64282
  var statusColors3;
64110
64283
  var init_TaskDetail = __esm(() => {
64284
+ init_lock_display();
64111
64285
  statusColors3 = {
64112
64286
  pending: "yellow",
64113
64287
  in_progress: "blue",
@@ -68182,7 +68356,7 @@ No task claimed (nothing available).`));
68182
68356
  });
68183
68357
  program2.command("assign <id> <agent>").description("Assign a task to an agent").option("-j, --json", "Output as JSON").option("--assign-seat", "Allow <agent> to name a durable seat (a seat queue has no session watching it)").action(async (id, agentInput, opts) => {
68184
68358
  const globalOpts = program2.opts();
68185
- const agent = await resolveValidatedAssignee(agentInput, Boolean(opts.assignSeat));
68359
+ const agent = await resolveValidatedAssignee(agentInput, Boolean(opts.assignSeat), (v) => `${id} ${v} --assign-seat`);
68186
68360
  const cloud = getTodosCloudClient();
68187
68361
  if (cloud) {
68188
68362
  try {