@hasna/todos 0.13.8 → 0.13.10

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.8",
2126
+ version: "0.13.10",
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 = ?");
@@ -18956,7 +19037,7 @@ var init_tasks = __esm(() => {
18956
19037
  function resolveClaimIdentity(verb, explicit) {
18957
19038
  const resolved = resolveWritableIdentity(explicit);
18958
19039
  if (!resolved.agent_id) {
18959
- handleError(new Error(`Cannot ${verb} a task without an agent identity: a claim writes locked_by, and a lock with no distinct holder is not a lock. ` + `Set TODOS_AGENT_ID=<name> for this session, or pass --agent <name>. ` + `An identity persisted by 'todos init' is deliberately not used here \u2014 it is keyed on $HOME and names the station, not this session.`));
19040
+ handleError(new Error(`Cannot ${verb} a task without an agent identity: this operation changes a task lock, and a lock with no distinct caller is not a lock. ` + `Set TODOS_AGENT_ID=<name> for this session, or pass --agent <name>. ` + `An identity persisted by 'todos init' is deliberately not used here \u2014 it is keyed on $HOME and names the station, not this session.`));
18960
19041
  }
18961
19042
  return resolved.agent_id;
18962
19043
  }
@@ -20736,8 +20817,11 @@ function registerTaskCommands(program2) {
20736
20817
  console.log(` ${chalk3.dim("Agent:")} ${task2.agent_id}`);
20737
20818
  if (task2.session_id)
20738
20819
  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})`);
20820
+ const showLock = lockDisplayState(task2.locked_by, task2.locked_at);
20821
+ if (showLock.held)
20822
+ console.log(` ${chalk3.dim("Locked:")} ${showLock.holder} (at ${showLock.lockedAt})`);
20823
+ else if (showLock.expired)
20824
+ console.log(` ${chalk3.dim("Lock:")} ${chalk3.dim(formatExpiredLock(showLock))}`);
20741
20825
  if (task2.requires_approval) {
20742
20826
  const approvalStatus = task2.approved_by ? chalk3.green(`approved by ${task2.approved_by}`) : chalk3.yellow("pending approval");
20743
20827
  console.log(` ${chalk3.dim("Approval:")} ${approvalStatus}`);
@@ -20880,8 +20964,11 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
20880
20964
  }
20881
20965
  if (task2.assigned_to)
20882
20966
  console.log(` ${chalk3.dim("Assigned:")} ${task2.assigned_to}`);
20883
- if (task2.locked_by)
20884
- console.log(` ${chalk3.dim("Locked by:")} ${task2.locked_by}`);
20967
+ const inspectLock = lockDisplayState(task2.locked_by, task2.locked_at);
20968
+ if (inspectLock.held)
20969
+ console.log(` ${chalk3.dim("Locked by:")} ${inspectLock.holder}`);
20970
+ else if (inspectLock.expired)
20971
+ console.log(` ${chalk3.dim("Lock:")} ${chalk3.dim(formatExpiredLock(inspectLock))}`);
20885
20972
  if (task2.project_id)
20886
20973
  console.log(` ${chalk3.dim("Project:")} ${task2.project_id}`);
20887
20974
  if (task2.plan_id)
@@ -21189,10 +21276,12 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21189
21276
  };
21190
21277
  const cloud = getTodosCloudClient();
21191
21278
  if (cloud) {
21279
+ const resolvedId2 = await resolveTaskIdForCommand(id, cloud);
21280
+ const agentId2 = resolveClaimIdentity("complete", globalOpts.agent);
21192
21281
  let task3;
21193
21282
  try {
21194
- task3 = await cloudCompleteTask(cloud, await resolveTaskIdForCommand(id, cloud), {
21195
- ...globalOpts.agent ? { agent_id: globalOpts.agent } : {},
21283
+ task3 = await cloudCompleteTask(cloud, resolvedId2, {
21284
+ agent_id: agentId2,
21196
21285
  ...completionOptions
21197
21286
  });
21198
21287
  } catch (e) {
@@ -21207,9 +21296,10 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21207
21296
  return;
21208
21297
  }
21209
21298
  const resolvedId = resolveTaskId(id);
21299
+ const agentId = resolveClaimIdentity("complete", globalOpts.agent);
21210
21300
  let task2;
21211
21301
  try {
21212
- task2 = completeTask(resolvedId, globalOpts.agent, undefined, completionOptions);
21302
+ task2 = completeTask(resolvedId, agentId, undefined, completionOptions);
21213
21303
  } catch (e) {
21214
21304
  handleError(e);
21215
21305
  }
@@ -21329,11 +21419,12 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21329
21419
  const globalOpts = program2.opts();
21330
21420
  const cloud = getTodosCloudClient();
21331
21421
  const resolvedId = cloud ? await resolveTaskIdForCommand(id, cloud) : resolveTaskId(id);
21422
+ const agentId = resolveClaimIdentity("unlock", globalOpts.agent);
21332
21423
  try {
21333
21424
  if (cloud)
21334
- await cloudUnlockTask(cloud, resolvedId, globalOpts.agent, !globalOpts.agent);
21425
+ await cloudUnlockTask(cloud, resolvedId, agentId);
21335
21426
  else
21336
- unlockTask(resolvedId, globalOpts.agent);
21427
+ unlockTask(resolvedId, agentId);
21337
21428
  } catch (e) {
21338
21429
  handleError(e);
21339
21430
  }
@@ -21491,6 +21582,7 @@ var init_task_commands = __esm(() => {
21491
21582
  init_task_lists();
21492
21583
  init_cloud_router();
21493
21584
  init_creator_identity();
21585
+ init_lock_display();
21494
21586
  init_claim_guard();
21495
21587
  init_assignee_guard();
21496
21588
  init_helpers();
@@ -26175,6 +26267,12 @@ function countProjectTasks(projectId) {
26175
26267
  FROM tasks
26176
26268
  WHERE project_id = ?`).get(projectId);
26177
26269
  }
26270
+ function countTasksForDeregistration(tasks) {
26271
+ return {
26272
+ total: tasks.length,
26273
+ incomplete: tasks.filter((task) => task.status !== "completed" && task.status !== "cancelled").length
26274
+ };
26275
+ }
26178
26276
  function pathIsWithinPrefix(projectPath, prefix) {
26179
26277
  const normalizedPath = resolve13(projectPath);
26180
26278
  const normalizedPrefix = resolve13(prefix);
@@ -26650,11 +26748,8 @@ function registerProjectCommands(program2) {
26650
26748
  return;
26651
26749
  }
26652
26750
  if (opts.deregister) {
26653
- if (cloud) {
26654
- handleError(new Error("REMOTE_COMMAND_UNSUPPORTED: projects --deregister has no safe /v1 equivalent; local SQLite fallback is disabled"));
26655
- }
26656
- const project = resolveExplicitProject(opts.deregister);
26657
- const counts = countProjectTasks(project.id);
26751
+ const project = cloud ? await cloudResolveProject(cloud, opts.deregister) : resolveExplicitProject(opts.deregister);
26752
+ const counts = cloud ? countTasksForDeregistration(await cloudListTasks(cloud, { project_id: project.id, include_subtasks: true })) : countProjectTasks(project.id);
26658
26753
  if (opts.pathPrefix && !pathIsWithinPrefix(project.path, opts.pathPrefix)) {
26659
26754
  handleError(new Error(`Refusing to deregister ${project.name}: path ${project.path} is not within ${opts.pathPrefix}`));
26660
26755
  }
@@ -26671,7 +26766,10 @@ function registerProjectCommands(program2) {
26671
26766
  tasks_preserved: true
26672
26767
  };
26673
26768
  if (!opts.dryRun) {
26674
- deleteProject(project.id);
26769
+ if (cloud)
26770
+ await cloudDeleteProject(cloud, project.id);
26771
+ else
26772
+ deleteProject(project.id);
26675
26773
  }
26676
26774
  if (globalOpts.json) {
26677
26775
  output(result, true);
@@ -31437,7 +31535,16 @@ class PostgresJsonRecordStore {
31437
31535
  updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString()
31438
31536
  }));
31439
31537
  }
31440
- buildTaskFilterSql(filter) {
31538
+ async resolveAssignedToAliases(ref) {
31539
+ const agent = await resolveAgentForAssignedFilter(ref, this);
31540
+ const aliases = new Set([ref]);
31541
+ if (agent) {
31542
+ aliases.add(agent.id);
31543
+ aliases.add(agent.name);
31544
+ }
31545
+ return [...aliases];
31546
+ }
31547
+ async buildTaskFilterSql(filter) {
31441
31548
  const params = [this.service, "tasks"];
31442
31549
  const conds = ["service = $1", "object_type = $2", "deleted_at IS NULL"];
31443
31550
  const p = (value) => {
@@ -31463,8 +31570,10 @@ class PostgresJsonRecordStore {
31463
31570
  conds.push(inClause("payload->>'status'", toFilterArray(filter.status)));
31464
31571
  if (filter.priority !== undefined)
31465
31572
  conds.push(inClause("payload->>'priority'", toFilterArray(filter.priority)));
31466
- if (filter.assigned_to !== undefined)
31467
- conds.push(`payload->>'assigned_to' = ${p(filter.assigned_to)}`);
31573
+ if (filter.assigned_to !== undefined) {
31574
+ const aliases = [...new Set((await this.resolveAssignedToAliases(filter.assigned_to)).map((a) => a.toLowerCase()))];
31575
+ conds.push(inClause("LOWER(payload->>'assigned_to')", aliases));
31576
+ }
31468
31577
  if (filter.agent_id !== undefined)
31469
31578
  conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
31470
31579
  if (filter.created_by !== undefined)
@@ -31498,7 +31607,7 @@ class PostgresJsonRecordStore {
31498
31607
  }
31499
31608
  async listTasks(filter) {
31500
31609
  await this.ensureSchema();
31501
- const { where, params, queryRef } = this.buildTaskFilterSql(filter);
31610
+ const { where, params, queryRef } = await this.buildTaskFilterSql(filter);
31502
31611
  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;
31503
31612
  let sql = `/* todos:list-tasks */ SELECT payload FROM ${this.tableName} WHERE ${where} ${orderBy}`;
31504
31613
  if (filter.limit !== undefined) {
@@ -31558,7 +31667,7 @@ class PostgresJsonRecordStore {
31558
31667
  }
31559
31668
  async countTasks(filter) {
31560
31669
  await this.ensureSchema();
31561
- const { where, params } = this.buildTaskFilterSql(filter);
31670
+ const { where, params } = await this.buildTaskFilterSql(filter);
31562
31671
  const sql = `/* todos:count-tasks */ SELECT COUNT(*)::int AS count FROM ${this.tableName} WHERE ${where}`;
31563
31672
  const result = await this.options.client.query(sql, params);
31564
31673
  return Number(result.rows[0]?.count ?? 0);
@@ -31719,9 +31828,13 @@ class PostgresJsonRecordStore {
31719
31828
  } : {};
31720
31829
  const hasEvidence = Object.keys(evidence).length > 0;
31721
31830
  const hasConfidence = options?.confidence !== undefined;
31722
- const result = await this.options.client.query(`/* todos:complete-task-atomic */ UPDATE ${this.tableName}
31831
+ const lockExpiryCutoff2 = new Date(new Date(operationTimestamp).getTime() - CLOUD_LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
31832
+ const result = await this.options.client.query(`/* todos:complete-task-atomic todos:complete-task-lock-guard todos:complete-task-clears-lock */
31833
+ UPDATE ${this.tableName}
31723
31834
  SET payload = payload || jsonb_build_object(
31724
31835
  'status', 'completed',
31836
+ 'locked_by', 'null'::jsonb,
31837
+ 'locked_at', 'null'::jsonb,
31725
31838
  'assigned_to', CASE
31726
31839
  WHEN jsonb_typeof(payload->'assigned_to') = 'string' THEN payload->'assigned_to'
31727
31840
  ELSE COALESCE(to_jsonb($3::text), 'null'::jsonb)
@@ -31749,6 +31862,16 @@ class PostgresJsonRecordStore {
31749
31862
  updated_at = $9::timestamptz,
31750
31863
  version = COALESCE(version, 0) + 1
31751
31864
  WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL
31865
+ AND (
31866
+ payload->>'locked_by' IS NULL
31867
+ OR BTRIM(payload->>'locked_by') = ''
31868
+ OR payload->>'locked_at' IS NULL
31869
+ OR payload->>'locked_at' < $10::text
31870
+ OR (
31871
+ $3::text IS NOT NULL
31872
+ AND LOWER(BTRIM(payload->>'locked_by')) = LOWER(BTRIM($3::text))
31873
+ )
31874
+ )
31752
31875
  RETURNING payload`, [
31753
31876
  this.service,
31754
31877
  id,
@@ -31758,7 +31881,8 @@ class PostgresJsonRecordStore {
31758
31881
  jsonbParam(evidence),
31759
31882
  hasConfidence,
31760
31883
  options?.confidence ?? null,
31761
- operationTimestamp
31884
+ operationTimestamp,
31885
+ lockExpiryCutoff2
31762
31886
  ]);
31763
31887
  return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
31764
31888
  }
@@ -32039,6 +32163,7 @@ async function updateTask2(id, input, store) {
32039
32163
  if (existing.version !== input.version) {
32040
32164
  throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
32041
32165
  }
32166
+ const reopened = existing.status === "completed" && input.status !== undefined && input.status !== "completed" && input.completed_at === undefined;
32042
32167
  const task = {
32043
32168
  ...existing,
32044
32169
  ...definedPatch(input),
@@ -32048,7 +32173,8 @@ async function updateTask2(id, input, store) {
32048
32173
  metadata: input.metadata ?? existing.metadata,
32049
32174
  requires_approval: input.requires_approval ?? existing.requires_approval,
32050
32175
  task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
32051
- created_by: existing.created_by
32176
+ created_by: existing.created_by,
32177
+ completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
32052
32178
  };
32053
32179
  await store.upsert("tasks", task);
32054
32180
  return task;
@@ -32056,11 +32182,11 @@ async function updateTask2(id, input, store) {
32056
32182
  async function startTask2(id, agentId, store) {
32057
32183
  const task = await requireRecord("tasks", id, store);
32058
32184
  if (task.status !== "pending" && task.status !== "in_progress") {
32059
- throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
32185
+ throw new TaskNotStartableError(task.id, task.status, agentId);
32060
32186
  }
32061
32187
  const started = await patchTask(task, {
32062
32188
  status: "in_progress",
32063
- assigned_to: task.assigned_to ?? agentId,
32189
+ assigned_to: task.assigned_to || agentId,
32064
32190
  agent_id: task.agent_id ?? agentId,
32065
32191
  locked_by: agentId,
32066
32192
  locked_at: new Date().toISOString(),
@@ -32071,8 +32197,13 @@ async function startTask2(id, agentId, store) {
32071
32197
  }
32072
32198
  async function completeTask2(id, agentId, options, store) {
32073
32199
  const task = await store.completeTask(id, agentId, options);
32074
- if (!task)
32200
+ if (!task) {
32201
+ const current = await store.get("tasks", id);
32202
+ if (current?.locked_by && !cloudLockExpired(current.locked_at) && !sameCloudLockHolder(current.locked_by, agentId)) {
32203
+ throw new LockError(id, current.locked_by);
32204
+ }
32075
32205
  throw new Error(`tasks record not found: ${id}`);
32206
+ }
32076
32207
  return task;
32077
32208
  }
32078
32209
  async function failTask2(id, agentId, reason, options, store) {
@@ -32113,6 +32244,11 @@ async function patchTask(task, patch, store) {
32113
32244
  await store.upsert("tasks", updated);
32114
32245
  return updated;
32115
32246
  }
32247
+ function sameCloudLockHolder(stored, incoming) {
32248
+ if (!stored || !incoming)
32249
+ return false;
32250
+ return canonicalAgentRef(stored) === canonicalAgentRef(incoming);
32251
+ }
32116
32252
  function cloudLockExpired(lockedAt) {
32117
32253
  if (!lockedAt)
32118
32254
  return true;
@@ -32430,6 +32566,16 @@ async function resolveAgent(idOrName, store) {
32430
32566
  return byId;
32431
32567
  return matchAgentByName(await store.list("agents"), idOrName);
32432
32568
  }
32569
+ async function resolveAgentForAssignedFilter(idOrName, store) {
32570
+ const byId = await store.get("agents", idOrName);
32571
+ if (byId)
32572
+ return byId;
32573
+ const target = normalizeAgentNameInput(idOrName);
32574
+ if (!target)
32575
+ return null;
32576
+ const matches = (await store.list("agents")).filter((agent) => normalizeAgentNameInput(agent.name) === target);
32577
+ return matches.length === 1 ? matches[0] : null;
32578
+ }
32433
32579
  async function heartbeatAgent(idOrName, store, context) {
32434
32580
  const agent = await resolveAgent(idOrName, store);
32435
32581
  if (!agent)
@@ -32745,6 +32891,7 @@ function postgresConstraintName(error) {
32745
32891
  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;
32746
32892
  var init_postgres_adapter = __esm(() => {
32747
32893
  init_types();
32894
+ init_creator_identity();
32748
32895
  init_postgres_sync();
32749
32896
  init_integrity();
32750
32897
  init_redaction();
@@ -34234,7 +34381,10 @@ function mapTaskError(e, json2) {
34234
34381
  retry_after: e.retryAfterSeconds ?? null
34235
34382
  }, 409);
34236
34383
  }
34237
- if (e instanceof Error && (/ is blocked by /.test(e.message) || /cannot be started/.test(e.message))) {
34384
+ if (e instanceof TaskNotStartableError) {
34385
+ return json2({ error: e.message, code: TaskNotStartableError.code }, 409);
34386
+ }
34387
+ if (e instanceof Error && / is blocked by /.test(e.message)) {
34238
34388
  return json2({ error: e.message, code: "TASK_NOT_STARTABLE" }, 409);
34239
34389
  }
34240
34390
  return null;
@@ -38228,10 +38378,10 @@ async function handleV1Request(req, url, dependencies = {}) {
38228
38378
  const released2 = await store.tasks.unlock(id);
38229
38379
  return json3({ success: released2 });
38230
38380
  }
38231
- if (body2.agent_id && principal.agent && body2.agent_id !== principal.agent && !principal.scopes.includes("todos:*")) {
38381
+ if (body2.agent_id && body2.agent_id !== principal.agent && !principal.scopes.includes("todos:*")) {
38232
38382
  return error(403, "unlock agent_id must match the authenticated agent");
38233
38383
  }
38234
- const agentId2 = principal.agent || body2.agent_id;
38384
+ const agentId2 = body2.agent_id || principal.agent;
38235
38385
  if (!agentId2)
38236
38386
  return error(403, "unlock requires an agent-bound key or force=true");
38237
38387
  const released = await store.tasks.unlock(id, agentId2);
@@ -38799,6 +38949,9 @@ async function handleV1Request(req, url, dependencies = {}) {
38799
38949
  }
38800
38950
  if (e instanceof LockError)
38801
38951
  return error(409, e.message, { code: LockError.code });
38952
+ if (e instanceof TaskNotStartableError) {
38953
+ return error(409, e.message, { code: TaskNotStartableError.code });
38954
+ }
38802
38955
  if (e instanceof ResourceConflictError)
38803
38956
  return error(409, e.message, { code: e.code, conflict: true });
38804
38957
  if (e instanceof ProjectNotFoundError)
@@ -62407,6 +62560,8 @@ var init_workflow_prompts2 = __esm(() => {
62407
62560
  // src/mcp/index.ts
62408
62561
  var exports_mcp = {};
62409
62562
  __export(exports_mcp, {
62563
+ formatTaskDetail: () => formatTaskDetail,
62564
+ formatTask: () => formatTask,
62410
62565
  buildServer: () => buildServer,
62411
62566
  applyFocus: () => applyFocus
62412
62567
  });
@@ -62555,7 +62710,8 @@ function resolveId(partialId, table = "tasks") {
62555
62710
  function formatTask(task2) {
62556
62711
  const id = task2.short_id || task2.id.slice(0, 8);
62557
62712
  const assigned = task2.assigned_to ? ` -> ${task2.assigned_to}` : "";
62558
- const lock = task2.locked_by ? ` [locked:${task2.locked_by}]` : "";
62713
+ const lockState = lockDisplayState(task2.locked_by, task2.locked_at);
62714
+ const lock = lockState.held ? ` [locked:${lockState.holder}]` : "";
62559
62715
  const recur = task2.recurrence_rule ? ` [\u21BB]` : "";
62560
62716
  return `${id} ${task2.status.padEnd(11)} ${task2.priority.padEnd(8)} ${task2.title}${assigned}${lock}${recur}`;
62561
62717
  }
@@ -62574,8 +62730,11 @@ function formatTaskDetail(task2, maxDescriptionChars) {
62574
62730
  parts.push(`Assigned to: ${task2.assigned_to}`);
62575
62731
  if (task2.agent_id)
62576
62732
  parts.push(`Agent: ${task2.agent_id}`);
62577
- if (task2.locked_by)
62578
- parts.push(`Locked by: ${task2.locked_by}`);
62733
+ const detailLock = lockDisplayState(task2.locked_by, task2.locked_at);
62734
+ if (detailLock.held)
62735
+ parts.push(`Locked by: ${detailLock.holder}`);
62736
+ else if (detailLock.expired)
62737
+ parts.push(`Lock: ${formatExpiredLock(detailLock)}`);
62579
62738
  if (task2.parent_id)
62580
62739
  parts.push(`Parent: ${task2.parent_id}`);
62581
62740
  if (task2.project_id)
@@ -62659,6 +62818,7 @@ var agentFocusMap, isDirectRun;
62659
62818
  var init_mcp2 = __esm(() => {
62660
62819
  init_agents();
62661
62820
  init_database();
62821
+ init_lock_display();
62662
62822
  init_types();
62663
62823
  init_dispatch2();
62664
62824
  init_task_crud2();
@@ -63704,7 +63864,7 @@ function TaskList({ tasks, selectedIndex }) {
63704
63864
  task2.assigned_to
63705
63865
  ]
63706
63866
  }, undefined, true, undefined, this),
63707
- task2.locked_by && /* @__PURE__ */ jsxDEV2(Text2, {
63867
+ lockDisplayState(task2.locked_by, task2.locked_at).held && /* @__PURE__ */ jsxDEV2(Text2, {
63708
63868
  color: "magenta",
63709
63869
  children: [
63710
63870
  " \uD83D\uDD12",
@@ -63726,6 +63886,7 @@ function TaskList({ tasks, selectedIndex }) {
63726
63886
  }
63727
63887
  var statusIcons, statusColors2, priorityLabels, priorityColors2;
63728
63888
  var init_TaskList = __esm(() => {
63889
+ init_lock_display();
63729
63890
  statusIcons = {
63730
63891
  pending: "\u25CB",
63731
63892
  in_progress: "\u25D0",
@@ -63888,7 +64049,7 @@ function TaskDetail({ task: task2 }) {
63888
64049
  }, undefined, false, undefined, this)
63889
64050
  ]
63890
64051
  }, undefined, true, undefined, this),
63891
- task2.locked_by && /* @__PURE__ */ jsxDEV3(Box3, {
64052
+ lockDisplayState(task2.locked_by, task2.locked_at).held && /* @__PURE__ */ jsxDEV3(Box3, {
63892
64053
  children: [
63893
64054
  /* @__PURE__ */ jsxDEV3(Text3, {
63894
64055
  dimColor: true,
@@ -63905,6 +64066,18 @@ function TaskDetail({ task: task2 }) {
63905
64066
  }, undefined, true, undefined, this)
63906
64067
  ]
63907
64068
  }, undefined, true, undefined, this),
64069
+ lockDisplayState(task2.locked_by, task2.locked_at).expired && /* @__PURE__ */ jsxDEV3(Box3, {
64070
+ children: [
64071
+ /* @__PURE__ */ jsxDEV3(Text3, {
64072
+ dimColor: true,
64073
+ children: "Lock: "
64074
+ }, undefined, false, undefined, this),
64075
+ /* @__PURE__ */ jsxDEV3(Text3, {
64076
+ dimColor: true,
64077
+ children: formatExpiredLock(lockDisplayState(task2.locked_by, task2.locked_at))
64078
+ }, undefined, false, undefined, this)
64079
+ ]
64080
+ }, undefined, true, undefined, this),
63908
64081
  task2.tags.length > 0 && /* @__PURE__ */ jsxDEV3(Box3, {
63909
64082
  children: [
63910
64083
  /* @__PURE__ */ jsxDEV3(Text3, {
@@ -64107,6 +64280,7 @@ function TaskDetail({ task: task2 }) {
64107
64280
  }
64108
64281
  var statusColors3;
64109
64282
  var init_TaskDetail = __esm(() => {
64283
+ init_lock_display();
64110
64284
  statusColors3 = {
64111
64285
  pending: "yellow",
64112
64286
  in_progress: "blue",
@@ -1 +1 @@
1
- {"version":3,"file":"stage-a.d.ts","sourceRoot":"","sources":["../../src/cli/stage-a.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAQ,MAAM,WAAW,CAAC;AAO1C,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE9C,MAAM,MAAM,+BAA+B,GACvC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,IAAI,CAAA;CAAE,GACrC;IAAE,KAAK,EAAE,mBAAmB,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC1D;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG,aAAa,GAAG,YAAY,CAAC;AAyB/E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;CA8BgB,CAAC;AAsBvD,wBAAgB,kCAAkC,IAAI,WAAW,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAE9F;AAED;;;;;;;GAOG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,+BAA+B,CAAC,OAAO,CAAC,GAC9C,OAAO,CAKT;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,+BAA+B,CAAC,OAAO,CAAC,GAAG,IAAI,CASnH;AAiTD;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,GAAE,MAAM,EAA0B,EACtC,GAAG,GAAE,GAAwB,GAC5B,+BAA+B,CAgBjC"}
1
+ {"version":3,"file":"stage-a.d.ts","sourceRoot":"","sources":["../../src/cli/stage-a.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAQ,MAAM,WAAW,CAAC;AAO1C,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE9C,MAAM,MAAM,+BAA+B,GACvC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,IAAI,CAAA;CAAE,GACrC;IAAE,KAAK,EAAE,mBAAmB,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC1D;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG,aAAa,GAAG,YAAY,CAAC;AAyB/E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;CA8BgB,CAAC;AAsBvD,wBAAgB,kCAAkC,IAAI,WAAW,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAE9F;AAED;;;;;;;GAOG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,+BAA+B,CAAC,OAAO,CAAC,GAC9C,OAAO,CAKT;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,+BAA+B,CAAC,OAAO,CAAC,GAAG,IAAI,CASnH;AAkTD;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,GAAE,MAAM,EAA0B,EACtC,GAAG,GAAE,GAAwB,GAC5B,+BAA+B,CAgBjC"}