@hasna/todos 0.13.12 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/commands/agent-commands.d.ts +39 -0
  3. package/dist/cli/commands/agent-commands.d.ts.map +1 -1
  4. package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/dispatch.d.ts.map +1 -1
  6. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  7. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  8. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  9. package/dist/cli/helpers.d.ts +64 -2
  10. package/dist/cli/helpers.d.ts.map +1 -1
  11. package/dist/cli/index.js +624 -98
  12. package/dist/cli/stage-a.d.ts.map +1 -1
  13. package/dist/contracts.js +123 -116
  14. package/dist/db/database.d.ts.map +1 -1
  15. package/dist/db/task-crud.d.ts.map +1 -1
  16. package/dist/index.js +135 -127
  17. package/dist/lib/bulk-tags.d.ts +57 -0
  18. package/dist/lib/bulk-tags.d.ts.map +1 -0
  19. package/dist/lib/enum-vocabulary.d.ts +72 -0
  20. package/dist/lib/enum-vocabulary.d.ts.map +1 -0
  21. package/dist/lib/run-records.d.ts.map +1 -1
  22. package/dist/lib/sandbox-profiles.d.ts.map +1 -1
  23. package/dist/lib/saved-search-views.d.ts +12 -1
  24. package/dist/lib/saved-search-views.d.ts.map +1 -1
  25. package/dist/lib/sync-utils.d.ts +1 -1
  26. package/dist/lib/sync-utils.d.ts.map +1 -1
  27. package/dist/mcp/index.d.ts.map +1 -1
  28. package/dist/mcp/index.js +289 -82
  29. package/dist/mcp/tools/agents.d.ts.map +1 -1
  30. package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
  31. package/dist/mcp/tools/task-crud.d.ts +1 -0
  32. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  33. package/dist/mcp.js +1 -1
  34. package/dist/registry.js +123 -116
  35. package/dist/release-provenance.json +5 -5
  36. package/dist/sdk/index.js +3 -2
  37. package/dist/sdk/v1.generated.d.ts +2 -2
  38. package/dist/sdk/v1.generated.d.ts.map +1 -1
  39. package/dist/server/index.js +245 -38
  40. package/dist/server/openapi.d.ts +58 -0
  41. package/dist/server/openapi.d.ts.map +1 -1
  42. package/dist/server/routes.d.ts.map +1 -1
  43. package/dist/server/v1.d.ts.map +1 -1
  44. package/dist/storage.js +124 -115
  45. package/dist/types/index.d.ts +11 -0
  46. package/dist/types/index.d.ts.map +1 -1
  47. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -2123,7 +2123,7 @@ var package_default;
2123
2123
  var init_package = __esm(() => {
2124
2124
  package_default = {
2125
2125
  name: "@hasna/todos",
2126
- version: "0.13.12",
2126
+ version: "0.15.1",
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",
@@ -2258,7 +2258,10 @@ 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, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
2261
+ function isTerminalStatus(status) {
2262
+ return status === "completed" || status === "failed" || status === "cancelled";
2263
+ }
2264
+ var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
2262
2265
  var init_types = __esm(() => {
2263
2266
  TASK_STATUSES = [
2264
2267
  "pending",
@@ -2427,6 +2430,7 @@ var init_types = __esm(() => {
2427
2430
  this.name = "CompletionGuardError";
2428
2431
  }
2429
2432
  };
2433
+ DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
2430
2434
  DispatchNotFoundError = class DispatchNotFoundError extends Error {
2431
2435
  dispatchId;
2432
2436
  static code = "DISPATCH_NOT_FOUND";
@@ -2440,11 +2444,30 @@ var init_types = __esm(() => {
2440
2444
  });
2441
2445
 
2442
2446
  // src/lib/sync-utils.ts
2447
+ var exports_sync_utils = {};
2448
+ __export(exports_sync_utils, {
2449
+ writeJsonFile: () => writeJsonFile,
2450
+ writeHighWaterMark: () => writeHighWaterMark,
2451
+ withSyncFingerprint: () => withSyncFingerprint,
2452
+ readJsonFile: () => readJsonFile,
2453
+ readHighWaterMark: () => readHighWaterMark,
2454
+ parseTimestamp: () => parseTimestamp,
2455
+ listJsonFiles: () => listJsonFiles,
2456
+ hasSyncFingerprintChanged: () => hasSyncFingerprintChanged,
2457
+ getTodosGlobalDir: () => getTodosGlobalDir,
2458
+ getHomeDir: () => getHomeDir,
2459
+ getFileMtimeMs: () => getFileMtimeMs,
2460
+ ensureDir: () => ensureDir,
2461
+ appendSyncConflict: () => appendSyncConflict,
2462
+ TODO_SYNC_FINGERPRINT_KEY: () => TODO_SYNC_FINGERPRINT_KEY,
2463
+ HOME: () => HOME
2464
+ });
2443
2465
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
2444
2466
  import { createHash } from "crypto";
2467
+ import { homedir } from "os";
2445
2468
  import { join } from "path";
2446
2469
  function getHomeDir() {
2447
- return process.env["HOME"] || process.env["USERPROFILE"] || "~";
2470
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
2448
2471
  }
2449
2472
  function getTodosGlobalDir() {
2450
2473
  return join(getHomeDir(), ".hasna", "todos");
@@ -2532,7 +2555,7 @@ function hasSyncFingerprintChanged(record) {
2532
2555
  }
2533
2556
  var TODO_SYNC_FINGERPRINT_KEY = "todos_sync_fingerprint", HOME;
2534
2557
  var init_sync_utils = __esm(() => {
2535
- HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
2558
+ HOME = getHomeDir();
2536
2559
  });
2537
2560
 
2538
2561
  // src/lib/config.ts
@@ -6410,6 +6433,15 @@ function hasOption(args, option) {
6410
6433
  function positionalArgs(args) {
6411
6434
  return args.filter((arg) => !arg.startsWith("-"));
6412
6435
  }
6436
+ function isBundledStaticInvocation(invocation) {
6437
+ const command = invocation.command;
6438
+ if (!command)
6439
+ return false;
6440
+ const storeBackedOptions = BUNDLED_STATIC_STORE_BACKED_OPTIONS.get(command);
6441
+ if (!storeBackedOptions)
6442
+ return false;
6443
+ return !storeBackedOptions.some((option) => hasOption(invocation.commandArgs, option));
6444
+ }
6413
6445
  function isReadOnlyConfigInvocation(invocation) {
6414
6446
  if (invocation.command !== "config")
6415
6447
  return false;
@@ -6430,6 +6462,8 @@ function isMetadataInvocation(args, invocation) {
6430
6462
  return true;
6431
6463
  if (invocation.command === "manual" && invocation.commandArgs.length === 0)
6432
6464
  return true;
6465
+ if (isBundledStaticInvocation(invocation))
6466
+ return true;
6433
6467
  if (invocation.command === "help" && invocation.commandArgs.every((arg) => !arg.startsWith("-")))
6434
6468
  return true;
6435
6469
  if (invocation.command === "config") {
@@ -6447,6 +6481,14 @@ function firstPresentOption(args, candidates) {
6447
6481
  function disqualifyingArgument(invocation) {
6448
6482
  const command = invocation.command;
6449
6483
  const args = invocation.commandArgs;
6484
+ const storeBackedOptions = BUNDLED_STATIC_STORE_BACKED_OPTIONS.get(command);
6485
+ if (storeBackedOptions) {
6486
+ const option = storeBackedOptions.find((candidate) => hasOption(args, candidate));
6487
+ return option ? {
6488
+ blame: option,
6489
+ remedy: `it imports bundled fixtures into the local store, which this route disables; re-run \`${command}\` without it to read the bundled content`
6490
+ } : null;
6491
+ }
6450
6492
  switch (command) {
6451
6493
  case "task":
6452
6494
  return positionalArgs(args)[0] === "upsert" ? null : { blame: "any subcommand other than `upsert`", remedy: "use `todos task upsert`" };
@@ -6474,13 +6516,22 @@ function disqualifyingArgument(invocation) {
6474
6516
  const action = positionalArgs(args)[0];
6475
6517
  if (action === "plan" || action === "move-plan")
6476
6518
  return null;
6519
+ if (action === "tag" || action === "untag") {
6520
+ if (!hasOption(args, "--tag") && !hasOption(args, "--tags")) {
6521
+ return {
6522
+ blame: `\`bulk ${action}\` without --tag`,
6523
+ remedy: `pass --tag <comma-separated>, e.g. bulk ${action} <ids...> --tag directive:k_msd4cz8t_ste6f4`
6524
+ };
6525
+ }
6526
+ return firstPresentOption(args, ["--plan", "--clear-plan"]);
6527
+ }
6477
6528
  if (!action) {
6478
- return { blame: "a missing action", remedy: "pass one of done, complete, start, delete, plan, move-plan" };
6529
+ return { blame: "a missing action", remedy: "pass one of done, complete, start, delete, plan, move-plan, tag, untag" };
6479
6530
  }
6480
6531
  if (!["done", "complete", "start", "delete"].includes(action)) {
6481
6532
  return {
6482
6533
  blame: `the \`${action}\` action`,
6483
- remedy: "use one of done, complete, start, delete, plan, move-plan"
6534
+ remedy: "use one of done, complete, start, delete, plan, move-plan, tag, untag"
6484
6535
  };
6485
6536
  }
6486
6537
  return firstPresentOption(args, ["--plan", "--clear-plan"]);
@@ -6537,7 +6588,8 @@ function assertRemoteCommandSupported(invocation) {
6537
6588
  }
6538
6589
  if (!command || !commandSupportsRemote(invocation)) {
6539
6590
  const blame = command ? disqualifyingArgument(invocation) : null;
6540
- const detail = blame ? `\`${command}\` is served by the Todos /v1 authority but ${blame.blame} is not; ${blame.remedy}` : `${invocationLabel(invocation)} is not supported by the Todos /v1 CLI; local SQLite fallback is disabled`;
6591
+ const servedBy = command && BUNDLED_STATIC_STORE_BACKED_OPTIONS.has(command) ? `\`${command}\` renders bundled content on this route` : `\`${command}\` is served by the Todos /v1 authority`;
6592
+ const detail = blame ? `${servedBy} but ${blame.blame} is not; ${blame.remedy}` : `${invocationLabel(invocation)} is not supported by the Todos /v1 CLI; local SQLite fallback is disabled`;
6541
6593
  throw new Error(`REMOTE_COMMAND_UNSUPPORTED: ${detail}`);
6542
6594
  }
6543
6595
  }
@@ -6557,7 +6609,7 @@ function initializeTodosCliAuthority(args = process.argv.slice(2), env = process
6557
6609
  }
6558
6610
  return { route: "remote-http", v1_base_url: client.baseUrl };
6559
6611
  }
6560
- var REGISTERED_CANONICAL_COMMANDS, TODOS_CLI_COMMAND_ALIASES, DIAGNOSTIC_COMMANDS, REMOTE_COMMANDS, COMMAND_CAPABILITY_MATRIX, GLOBAL_OPTIONS_WITH_VALUES, GLOBAL_FLAGS, HELP_FLAGS, VERSION_FLAGS, dropIt = (blame) => ({ blame, remedy: "re-run without it" });
6612
+ var REGISTERED_CANONICAL_COMMANDS, TODOS_CLI_COMMAND_ALIASES, BUNDLED_STATIC_COMMANDS, BUNDLED_STATIC_STORE_BACKED_OPTIONS, DIAGNOSTIC_COMMANDS, REMOTE_COMMANDS, COMMAND_CAPABILITY_MATRIX, GLOBAL_OPTIONS_WITH_VALUES, GLOBAL_FLAGS, HELP_FLAGS, VERSION_FLAGS, dropIt = (blame) => ({ blame, remedy: "re-run without it" });
6561
6613
  var init_stage_a = __esm(() => {
6562
6614
  init_esm();
6563
6615
  init_cloud_router();
@@ -6747,7 +6799,28 @@ var init_stage_a = __esm(() => {
6747
6799
  reliability: ["scorecards"],
6748
6800
  lists: ["task-lists", "tl"]
6749
6801
  };
6750
- DIAGNOSTIC_COMMANDS = new Set(["help", "manual", "completions", "completion", "config", "storage"]);
6802
+ BUNDLED_STATIC_COMMANDS = {
6803
+ workflows: [],
6804
+ "template-library": [],
6805
+ onboarding: ["--import"],
6806
+ "sdk-fixtures": ["--show", "--write"]
6807
+ };
6808
+ BUNDLED_STATIC_STORE_BACKED_OPTIONS = new Map;
6809
+ for (const [canonical, storeBackedOptions] of Object.entries(BUNDLED_STATIC_COMMANDS)) {
6810
+ BUNDLED_STATIC_STORE_BACKED_OPTIONS.set(canonical, storeBackedOptions);
6811
+ const aliases = TODOS_CLI_COMMAND_ALIASES[canonical] ?? [];
6812
+ for (const alias of aliases)
6813
+ BUNDLED_STATIC_STORE_BACKED_OPTIONS.set(alias, storeBackedOptions);
6814
+ }
6815
+ DIAGNOSTIC_COMMANDS = new Set([
6816
+ "help",
6817
+ "manual",
6818
+ "completions",
6819
+ "completion",
6820
+ "config",
6821
+ "storage",
6822
+ ...Object.keys(BUNDLED_STATIC_COMMANDS)
6823
+ ]);
6751
6824
  REMOTE_COMMANDS = new Set([
6752
6825
  "active",
6753
6826
  "add",
@@ -10769,8 +10842,7 @@ function findGitRoot(startDir) {
10769
10842
  return null;
10770
10843
  }
10771
10844
  function getGlobalDbPath() {
10772
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
10773
- return join3(home, ".hasna", "todos", "todos.db");
10845
+ return join3(getHomeDir(), ".hasna", "todos", "todos.db");
10774
10846
  }
10775
10847
  function hasExplicitProjectArg(args = process.argv.slice(2)) {
10776
10848
  return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
@@ -10992,6 +11064,7 @@ var init_database = __esm(() => {
10992
11064
  init_machines();
10993
11065
  init_identity_mapping();
10994
11066
  init_types();
11067
+ init_sync_utils();
10995
11068
  ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
10996
11069
  });
10997
11070
 
@@ -11456,6 +11529,103 @@ var init_lock_display = __esm(() => {
11456
11529
  init_database();
11457
11530
  });
11458
11531
 
11532
+ // src/lib/enum-vocabulary.ts
11533
+ function editDistance2(a, b) {
11534
+ if (a === b)
11535
+ return 0;
11536
+ if (!a.length)
11537
+ return b.length;
11538
+ if (!b.length)
11539
+ return a.length;
11540
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
11541
+ for (let i = 1;i <= a.length; i += 1) {
11542
+ const current = [i];
11543
+ for (let j = 1;j <= b.length; j += 1) {
11544
+ current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
11545
+ }
11546
+ previous = current;
11547
+ }
11548
+ return previous[b.length];
11549
+ }
11550
+ function suggestVocabularyMatches(value, vocabulary, limit = 3) {
11551
+ const needle = value.trim().toLowerCase();
11552
+ if (!needle)
11553
+ return [];
11554
+ const scored = [];
11555
+ for (const member of vocabulary) {
11556
+ const candidate = member.toLowerCase();
11557
+ if (candidate.startsWith(needle) || needle.startsWith(candidate)) {
11558
+ scored.push({ member, score: 0 });
11559
+ continue;
11560
+ }
11561
+ if (candidate.includes(needle) || needle.includes(candidate)) {
11562
+ scored.push({ member, score: 1 });
11563
+ continue;
11564
+ }
11565
+ const distance = editDistance2(needle, candidate);
11566
+ if (distance <= Math.max(1, Math.floor(candidate.length / 3))) {
11567
+ scored.push({ member, score: 1 + distance });
11568
+ }
11569
+ }
11570
+ return scored.sort((a, b) => a.score - b.score || a.member.localeCompare(b.member)).slice(0, limit).map((entry) => entry.member);
11571
+ }
11572
+ function resolveEnumVocabulary(raw, spec) {
11573
+ const allowList = spec.allowList !== false;
11574
+ const rawElements = (allowList ? raw.split(",") : [raw]).map((element) => element.trim());
11575
+ if (rawElements.every((element) => element.length === 0)) {
11576
+ return {
11577
+ ok: false,
11578
+ message: `${spec.name} requires a value. Allowed values: ${spec.vocabulary.join(", ")}.`,
11579
+ invalid: []
11580
+ };
11581
+ }
11582
+ if (rawElements.some((element) => element.length === 0)) {
11583
+ return {
11584
+ ok: false,
11585
+ message: `${spec.name} has an empty value in "${raw}" \u2014 remove the stray comma. ` + `Allowed values: ${spec.vocabulary.join(", ")}.`,
11586
+ invalid: []
11587
+ };
11588
+ }
11589
+ const normalized = rawElements.map((element) => spec.normalize ? spec.normalize(element) : element);
11590
+ const allowed = new Set(spec.vocabulary);
11591
+ const invalid2 = [];
11592
+ for (let i = 0;i < normalized.length; i += 1) {
11593
+ if (!allowed.has(normalized[i]))
11594
+ invalid2.push(rawElements[i]);
11595
+ }
11596
+ if (invalid2.length === 0) {
11597
+ return { ok: true, values: [...new Set(normalized)] };
11598
+ }
11599
+ const label = invalid2.length === 1 ? "value" : "values";
11600
+ const parts = [
11601
+ `Invalid ${spec.name} ${label}: ${invalid2.join(", ")}.`,
11602
+ `Allowed values: ${spec.vocabulary.join(", ")}.`
11603
+ ];
11604
+ const hints = new Set;
11605
+ const suggestions = new Set;
11606
+ for (let i = 0;i < normalized.length; i += 1) {
11607
+ const canonical = normalized[i];
11608
+ if (allowed.has(canonical))
11609
+ continue;
11610
+ const hint = spec.hints?.[canonical.toLowerCase()];
11611
+ if (hint) {
11612
+ hints.add(hint);
11613
+ continue;
11614
+ }
11615
+ for (const match of suggestVocabularyMatches(canonical, spec.vocabulary)) {
11616
+ suggestions.add(match);
11617
+ }
11618
+ }
11619
+ if (suggestions.size > 0)
11620
+ parts.push(`Did you mean ${[...suggestions].join(", ")}?`);
11621
+ for (const hint of hints)
11622
+ parts.push(hint);
11623
+ return { ok: false, message: parts.join(" "), invalid: invalid2 };
11624
+ }
11625
+ function collapseEnumValues(values) {
11626
+ return values.length === 1 ? values[0] : values;
11627
+ }
11628
+
11459
11629
  // src/cli/helpers.ts
11460
11630
  var exports_helpers = {};
11461
11631
  __export(exports_helpers, {
@@ -11466,9 +11636,13 @@ __export(exports_helpers, {
11466
11636
  resolveExplicitProject: () => resolveExplicitProject,
11467
11637
  priorityColors: () => priorityColors,
11468
11638
  printJson: () => printJson,
11639
+ parseEnumFlagList: () => parseEnumFlagList,
11640
+ parseEnumFlag: () => parseEnumFlag,
11641
+ outputRecord: () => outputRecord,
11469
11642
  output: () => output,
11470
11643
  normalizeStatusList: () => normalizeStatusList,
11471
11644
  normalizeStatus: () => normalizeStatus,
11645
+ normalizePriority: () => normalizePriority,
11472
11646
  jsonModeRequested: () => jsonModeRequested,
11473
11647
  handleError: () => handleError,
11474
11648
  getPackageVersion: () => getPackageVersion,
@@ -11476,7 +11650,10 @@ __export(exports_helpers, {
11476
11650
  detectGitRoot: () => detectGitRoot,
11477
11651
  cacheCloudTaskForIdResolution: () => cacheCloudTaskForIdResolution,
11478
11652
  autoProject: () => autoProject,
11479
- autoDetectProject: () => autoDetectProject
11653
+ autoDetectProject: () => autoDetectProject,
11654
+ TASK_STATUS_FLAG_HINTS: () => TASK_STATUS_FLAG_HINTS,
11655
+ TASK_STATUS_FLAG: () => TASK_STATUS_FLAG,
11656
+ TASK_PRIORITY_FLAG: () => TASK_PRIORITY_FLAG
11480
11657
  });
11481
11658
  import chalk from "chalk";
11482
11659
  import { execSync } from "child_process";
@@ -11486,8 +11663,19 @@ import { dirname as dirname3, join as join4, resolve as resolve3, sep } from "pa
11486
11663
  function jsonModeRequested(argv = process.argv) {
11487
11664
  return argv.some((arg) => arg === "--json" || /^-[a-z]+$/i.test(arg) && arg.includes("j"));
11488
11665
  }
11666
+ function remoteErrorDetail(e) {
11667
+ if (!e || typeof e !== "object")
11668
+ return null;
11669
+ const body = e.body;
11670
+ if (!body || typeof body !== "object" || Array.isArray(body))
11671
+ return null;
11672
+ const detail = body.error;
11673
+ return typeof detail === "string" && detail.trim() ? detail.trim() : null;
11674
+ }
11489
11675
  function handleError(e) {
11490
- const message = e instanceof Error ? e.message : String(e);
11676
+ const baseMessage = e instanceof Error ? e.message : String(e);
11677
+ const detail = remoteErrorDetail(e);
11678
+ const message = detail && !baseMessage.includes(detail) ? `${baseMessage}: ${detail}` : baseMessage;
11491
11679
  console.error(chalk.red(message));
11492
11680
  if (jsonModeRequested()) {
11493
11681
  console.log(JSON.stringify({ error: message }));
@@ -11665,7 +11853,8 @@ function autoProject(opts) {
11665
11853
  return autoDetectProject(opts)?.id;
11666
11854
  }
11667
11855
  function normalizeStatus(s) {
11668
- switch (s.toLowerCase().trim()) {
11856
+ const folded = s.toLowerCase().trim();
11857
+ switch (folded) {
11669
11858
  case "done":
11670
11859
  return "completed";
11671
11860
  case "complete":
@@ -11679,14 +11868,33 @@ function normalizeStatus(s) {
11679
11868
  case "canceled":
11680
11869
  return "cancelled";
11681
11870
  default:
11682
- return s;
11871
+ return folded;
11683
11872
  }
11684
11873
  }
11874
+ function normalizePriority(p) {
11875
+ return p.toLowerCase().trim();
11876
+ }
11685
11877
  function normalizeStatusList(statuses) {
11686
11878
  if (Array.isArray(statuses))
11687
11879
  return statuses.map(normalizeStatus);
11688
11880
  return normalizeStatus(statuses);
11689
11881
  }
11882
+ function parseEnumFlag(raw, spec) {
11883
+ if (raw === undefined || raw === null)
11884
+ return;
11885
+ const result = resolveEnumVocabulary(String(raw), spec);
11886
+ if (!result.ok)
11887
+ handleError(new Error(result.message));
11888
+ return collapseEnumValues(result.values);
11889
+ }
11890
+ function parseEnumFlagList(raw, spec) {
11891
+ if (raw === undefined || raw === null)
11892
+ return;
11893
+ const result = resolveEnumVocabulary(String(raw), spec);
11894
+ if (!result.ok)
11895
+ handleError(new Error(result.message));
11896
+ return result.values;
11897
+ }
11690
11898
  function writeStdoutSync(text) {
11691
11899
  const buffer = Buffer.from(text);
11692
11900
  let offset = 0;
@@ -11718,6 +11926,41 @@ function output(data, jsonMode) {
11718
11926
  printJson(data);
11719
11927
  }
11720
11928
  }
11929
+ function formatRecordLines(data) {
11930
+ if (data === null || data === undefined)
11931
+ return [];
11932
+ if (typeof data !== "object")
11933
+ return [String(data)];
11934
+ if (Array.isArray(data)) {
11935
+ return data.flatMap((entry, index) => [
11936
+ chalk.dim(`[${index}]`),
11937
+ ...formatRecordLines(entry).map((line) => ` ${line}`)
11938
+ ]);
11939
+ }
11940
+ const lines = [];
11941
+ for (const [key, value] of Object.entries(data)) {
11942
+ if (value === undefined)
11943
+ continue;
11944
+ const rendered = value === null ? chalk.dim("\u2014") : typeof value === "object" ? JSON.stringify(value) : String(value);
11945
+ lines.push(` ${chalk.dim(`${key}:`)} ${rendered}`);
11946
+ }
11947
+ return lines;
11948
+ }
11949
+ function outputRecord(data, jsonMode, heading) {
11950
+ if (jsonMode) {
11951
+ printJson(data);
11952
+ return;
11953
+ }
11954
+ if (heading)
11955
+ console.log(chalk.bold(heading));
11956
+ const lines = formatRecordLines(data);
11957
+ if (lines.length === 0) {
11958
+ console.log(chalk.dim("(empty)"));
11959
+ return;
11960
+ }
11961
+ for (const line of lines)
11962
+ console.log(line);
11963
+ }
11721
11964
  function formatTaskLine(t) {
11722
11965
  const statusFn = statusColors[t.status] || chalk.white;
11723
11966
  const priorityFn = priorityColors[t.priority] || chalk.white;
@@ -11728,16 +11971,33 @@ function formatTaskLine(t) {
11728
11971
  const plan = t.plan_id ? chalk.magenta(` [plan:${t.plan_id.slice(0, 8)}]`) : "";
11729
11972
  return `${chalk.dim(t.id.slice(0, 8))} ${statusFn(t.status.padEnd(11))} ${priorityFn(t.priority.padEnd(8))} ${t.title}${assigned}${lock}${tags}${plan}`;
11730
11973
  }
11731
- var stdoutRetryBuffer, stdoutRetrySignal, TASK_UUID_RE, statusColors, priorityColors;
11974
+ var stdoutRetryBuffer, stdoutRetrySignal, TASK_UUID_RE, TASK_STATUS_FLAG_HINTS, TASK_STATUS_FLAG, TASK_PRIORITY_FLAG, statusColors, priorityColors;
11732
11975
  var init_helpers = __esm(() => {
11733
11976
  init_cloud_router();
11734
11977
  init_database();
11735
11978
  init_projects();
11736
11979
  init_lock_display();
11737
11980
  init_package_version();
11981
+ init_types();
11738
11982
  stdoutRetryBuffer = new SharedArrayBuffer(4);
11739
11983
  stdoutRetrySignal = new Int32Array(stdoutRetryBuffer);
11740
11984
  TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11985
+ TASK_STATUS_FLAG_HINTS = {
11986
+ all: "To include every status, use -a/--all instead of a --status value.",
11987
+ any: "To include every status, use -a/--all instead of a --status value.",
11988
+ open: "Unstarted and running work is pending and in_progress (the default when no --status is given)."
11989
+ };
11990
+ TASK_STATUS_FLAG = {
11991
+ name: "--status",
11992
+ vocabulary: TASK_STATUSES,
11993
+ normalize: normalizeStatus,
11994
+ hints: TASK_STATUS_FLAG_HINTS
11995
+ };
11996
+ TASK_PRIORITY_FLAG = {
11997
+ name: "--priority",
11998
+ vocabulary: TASK_PRIORITIES,
11999
+ normalize: normalizePriority
12000
+ };
11741
12001
  statusColors = {
11742
12002
  pending: chalk.yellow,
11743
12003
  in_progress: chalk.blue,
@@ -12523,7 +12783,7 @@ var init_event_hooks = __esm(() => {
12523
12783
  // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
12524
12784
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
12525
12785
  import { existsSync as existsSync6 } from "fs";
12526
- import { homedir } from "os";
12786
+ import { homedir as homedir2 } from "os";
12527
12787
  import { join as join5 } from "path";
12528
12788
  import { createHmac, timingSafeEqual } from "crypto";
12529
12789
  import { randomUUID as randomUUID2 } from "crypto";
@@ -12625,7 +12885,7 @@ function channelMatchesEvent(channel, event) {
12625
12885
  return channel.filters.some((filter) => eventMatchesFilter(event, filter));
12626
12886
  }
12627
12887
  function getEventsDataDir(override) {
12628
- return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(homedir(), ".hasna", "events");
12888
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(homedir2(), ".hasna", "events");
12629
12889
  }
12630
12890
 
12631
12891
  class JsonEventsStore {
@@ -15960,11 +16220,13 @@ function updateTask(id, input, db) {
15960
16220
  }
15961
16221
  sets.push("status = ?");
15962
16222
  params.push(input.status);
16223
+ if (isTerminalStatus(input.status)) {
16224
+ sets.push("locked_by = NULL");
16225
+ sets.push("locked_at = NULL");
16226
+ }
15963
16227
  if (input.status === "completed") {
15964
16228
  sets.push("completed_at = ?");
15965
16229
  params.push(completionTimestamp);
15966
- sets.push("locked_by = NULL");
15967
- sets.push("locked_at = NULL");
15968
16230
  } else if (task.status === "completed" && input.completed_at === undefined) {
15969
16231
  sets.push("completed_at = NULL");
15970
16232
  }
@@ -16088,6 +16350,7 @@ function updateTask(id, input, db) {
16088
16350
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
16089
16351
  const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
16090
16352
  const completedNow = input.status === "completed";
16353
+ const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
16091
16354
  const updatedTask = {
16092
16355
  ...task,
16093
16356
  ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
@@ -16095,8 +16358,8 @@ function updateTask(id, input, db) {
16095
16358
  metadata: input.metadata ?? task.metadata,
16096
16359
  version: task.version + 1,
16097
16360
  updated_at: timestamp2,
16098
- locked_by: completedNow ? null : task.locked_by,
16099
- locked_at: completedNow ? null : task.locked_at,
16361
+ locked_by: terminalNow ? null : task.locked_by,
16362
+ locked_at: terminalNow ? null : task.locked_at,
16100
16363
  completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
16101
16364
  sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
16102
16365
  actual_minutes: input.actual_minutes ?? task.actual_minutes,
@@ -19054,6 +19317,49 @@ var init_tasks = __esm(() => {
19054
19317
  init_calendar();
19055
19318
  });
19056
19319
 
19320
+ // src/lib/bulk-tags.ts
19321
+ function parseTagList(raw) {
19322
+ if (!raw)
19323
+ return [];
19324
+ const seen = new Set;
19325
+ const tags = [];
19326
+ for (const segment of raw.split(",")) {
19327
+ const tag = segment.trim();
19328
+ if (!tag || seen.has(tag))
19329
+ continue;
19330
+ seen.add(tag);
19331
+ tags.push(tag);
19332
+ }
19333
+ return tags;
19334
+ }
19335
+ function resolveTagArgument(tag, tags) {
19336
+ if (tag === undefined)
19337
+ return { ok: true, raw: tags };
19338
+ if (tags === undefined)
19339
+ return { ok: true, raw: tag };
19340
+ const left = parseTagList(tag);
19341
+ const right = parseTagList(tags);
19342
+ const same = left.length === right.length && left.every((entry) => right.includes(entry));
19343
+ if (!same)
19344
+ return { ok: false, conflict: { tag, tags } };
19345
+ return { ok: true, raw: tag };
19346
+ }
19347
+ function resolveBulkTags(current, action, tags) {
19348
+ const existing = Array.isArray(current) ? current.filter((tag) => typeof tag === "string") : [];
19349
+ if (tags.length === 0)
19350
+ return { tags: [...existing], changed: false };
19351
+ if (action === "tag") {
19352
+ const present = new Set(existing);
19353
+ const additions = tags.filter((tag) => !present.has(tag));
19354
+ if (additions.length === 0)
19355
+ return { tags: [...existing], changed: false };
19356
+ return { tags: [...existing, ...additions], changed: true };
19357
+ }
19358
+ const removing = new Set(tags);
19359
+ const remaining = existing.filter((tag) => !removing.has(tag));
19360
+ return { tags: remaining, changed: remaining.length !== existing.length };
19361
+ }
19362
+
19057
19363
  // src/cli/claim-guard.ts
19058
19364
  function resolveClaimIdentity(verb, explicit) {
19059
19365
  const resolved = resolveWritableIdentity(explicit);
@@ -19074,10 +19380,10 @@ function normalizeAgentNameInput(name) {
19074
19380
 
19075
19381
  // src/lib/assignee-validation.ts
19076
19382
  import { readFileSync as readFileSync4 } from "fs";
19077
- import { homedir as homedir2 } from "os";
19383
+ import { homedir as homedir3 } from "os";
19078
19384
  import { join as join8 } from "path";
19079
19385
  function defaultSeatRosterPath() {
19080
- return process.env["TODOS_SEAT_ROSTER_PATH"] || join8(homedir2(), ".hasna", "identities", "hasna-seats.roster.json");
19386
+ return process.env["TODOS_SEAT_ROSTER_PATH"] || join8(homedir3(), ".hasna", "identities", "hasna-seats.roster.json");
19081
19387
  }
19082
19388
  function loadSeatSlugs(path = defaultSeatRosterPath()) {
19083
19389
  try {
@@ -20211,14 +20517,30 @@ function resolveProjectIdOrSlug(input) {
20211
20517
  return row.id;
20212
20518
  handleError(new Error(`Project not found: ${input}`));
20213
20519
  }
20520
+ function projectRefFromOpts(opts, globalOpts) {
20521
+ const own = typeof opts.project === "string" ? opts.project : undefined;
20522
+ const global = typeof globalOpts.project === "string" ? globalOpts.project : undefined;
20523
+ return own || global;
20524
+ }
20525
+ function projectOptOut(opts) {
20526
+ return opts.project === false;
20527
+ }
20528
+ function warnMissingProject(resolvedProjectId, optedOut) {
20529
+ if (resolvedProjectId || optedOut)
20530
+ return;
20531
+ console.error(chalk3.yellow("Warning: task filed with no project \u2014 it will not appear in any project list, per-seat report, or drain. " + "Pass --project <id-or-slug>, or --no-project if filing it globally is deliberate."));
20532
+ }
20214
20533
  function parseStatus(value) {
20215
20534
  if (!value)
20216
20535
  return;
20217
- const normalized = normalizeStatus(value);
20218
- if (!TASK_STATUSES.includes(normalized)) {
20219
- handleError(new Error(`--status must be one of: ${TASK_STATUSES.join(", ")}`));
20220
- }
20221
- return normalized;
20536
+ return parseEnumFlagList(value, { ...TASK_STATUS_FLAG, allowList: false })?.[0];
20537
+ }
20538
+ function listScanLimit() {
20539
+ const raw = process.env["TODOS_LIST_SCAN_LIMIT"];
20540
+ if (raw === undefined || raw.trim() === "")
20541
+ return DEFAULT_LIST_SCAN_LIMIT;
20542
+ const parsed = Number.parseInt(raw, 10);
20543
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_LIST_SCAN_LIMIT;
20222
20544
  }
20223
20545
  function parseIntOption(value, flag) {
20224
20546
  if (value === undefined)
@@ -20243,10 +20565,7 @@ function resolvePlanId(input) {
20243
20565
  function parsePriority(value) {
20244
20566
  if (!value)
20245
20567
  return;
20246
- if (!TASK_PRIORITIES.includes(value)) {
20247
- handleError(new Error("--priority must be one of: low, medium, high, critical"));
20248
- }
20249
- return value;
20568
+ return parseEnumFlagList(value, { ...TASK_PRIORITY_FLAG, allowList: false })?.[0];
20250
20569
  }
20251
20570
  function parseJsonObject3(value, flag) {
20252
20571
  if (!value)
@@ -20360,7 +20679,7 @@ function computeLocalReparent(current, opts) {
20360
20679
  return patch;
20361
20680
  }
20362
20681
  function registerTaskCommands(program2) {
20363
- program2.command("add <title>").description("Create a new task").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("--parent <id>", "Parent task ID").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--plan <id>", "Assign to a plan").option("--assign <agent>", "Assign to agent").option("--status <status>", "Initial status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--approval", "Require approval before completion").option("--recurrence <rule>", "Recurrence rule, e.g. 'every day', 'every weekday', 'every 2 weeks'").option("--due <date>", "Due date (ISO string or YYYY-MM-DD)").option("--reason <text>", "Why this task exists").option("--project <id>", "Assign to project by ID or slug (overrides auto-detect)").option("--unassigned", "Deliberately file this task with no assignee").option("--assign-seat", "Allow --assign to name a durable seat (a seat queue has no session watching it)").option("--created-by <agent>", "Record a different filer than the resolved agent identity").action(async (title, opts) => {
20682
+ program2.command("add <title>").description("Create a new task").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("--parent <id>", "Parent task ID").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--plan <id>", "Assign to a plan").option("--assign <agent>", "Assign to agent").option("--status <status>", "Initial status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--approval", "Require approval before completion").option("--recurrence <rule>", "Recurrence rule, e.g. 'every day', 'every weekday', 'every 2 weeks'").option("--due <date>", "Due date (ISO string or YYYY-MM-DD)").option("--reason <text>", "Why this task exists").option("--project <id>", "Assign to project by ID or slug (overrides auto-detect)").option("--no-project", "Deliberately file this task with no project (silences the orphan warning)").option("--unassigned", "Deliberately file this task with no assignee").option("--assign-seat", "Allow --assign to name a durable seat (a seat queue has no session watching it)").option("--created-by <agent>", "Record a different filer than the resolved agent identity").action(async (title, opts) => {
20364
20683
  const globalOpts = program2.opts();
20365
20684
  opts.tags = opts.tags || opts.tag;
20366
20685
  opts.list = opts.list || opts.taskList;
@@ -20375,8 +20694,9 @@ function registerTaskCommands(program2) {
20375
20694
  if (cloud) {
20376
20695
  let task3;
20377
20696
  try {
20378
- const cloudProjectRef = opts.project || globalOpts.project;
20697
+ const cloudProjectRef = projectRefFromOpts(opts, globalOpts);
20379
20698
  const cloudProjectId = cloudProjectRef ? await cloudResolveProjectRef(cloud, cloudProjectRef) : undefined;
20699
+ warnMissingProject(cloudProjectId, projectOptOut(opts));
20380
20700
  const cloudTaskListId = opts.list ? await cloudResolveTaskListRef(cloud, opts.list, cloudProjectId) : undefined;
20381
20701
  if (opts.list && !cloudTaskListId) {
20382
20702
  throw new Error(`Could not resolve task list ID or slug: ${opts.list}`);
@@ -20399,6 +20719,7 @@ function registerTaskCommands(program2) {
20399
20719
  created_by: creator.agent_id || undefined,
20400
20720
  session_id: globalOpts.session,
20401
20721
  project_id: cloudProjectId,
20722
+ working_dir: process.cwd(),
20402
20723
  estimated_minutes: opts.estimated !== undefined ? parseIntOption(opts.estimated, "--estimated") : undefined,
20403
20724
  sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseIntOption(opts.slaMinutes ?? opts.sla, "--sla-minutes") : undefined,
20404
20725
  requires_approval: opts.approval || undefined,
@@ -20417,8 +20738,9 @@ function registerTaskCommands(program2) {
20417
20738
  }
20418
20739
  return;
20419
20740
  }
20420
- const explicitProject = opts.project || globalOpts.project;
20421
- const projectId = explicitProject ? resolveProjectIdOrSlug(explicitProject) : autoProject(globalOpts);
20741
+ const explicitProject = projectRefFromOpts(opts, globalOpts);
20742
+ const projectId = explicitProject ? resolveProjectIdOrSlug(explicitProject) : projectOptOut(opts) ? undefined : autoProject(globalOpts);
20743
+ warnMissingProject(projectId, projectOptOut(opts));
20422
20744
  opts.tags = opts.tags || opts.tag;
20423
20745
  opts.list = opts.list || opts.taskList;
20424
20746
  const taskListId = opts.list ? (() => {
@@ -20630,15 +20952,19 @@ function registerTaskCommands(program2) {
20630
20952
  }
20631
20953
  filter["task_list_id"] = listId;
20632
20954
  }
20633
- if (opts.status) {
20634
- filter["status"] = opts.status.includes(",") ? opts.status.split(",").map((s) => normalizeStatus(s.trim())) : normalizeStatus(opts.status);
20955
+ if (opts.status !== undefined) {
20956
+ filter["status"] = parseEnumFlag(opts.status, TASK_STATUS_FLAG);
20635
20957
  } else if (!opts.all) {
20636
20958
  filter["status"] = ["pending", "in_progress"];
20637
20959
  }
20638
- if (opts.priority)
20639
- filter["priority"] = opts.priority;
20640
- if (opts.assigned)
20641
- filter["assigned_to"] = opts.assigned;
20960
+ if (opts.priority !== undefined)
20961
+ filter["priority"] = parseEnumFlag(opts.priority, TASK_PRIORITY_FLAG);
20962
+ let assignedFilter;
20963
+ let assignedWasTyped = false;
20964
+ if (opts.assigned) {
20965
+ assignedFilter = opts.assigned;
20966
+ assignedWasTyped = true;
20967
+ }
20642
20968
  if (opts.createdBy)
20643
20969
  filter["created_by"] = canonicalAgentRef(opts.createdBy);
20644
20970
  if (opts.notCreatedBy)
@@ -20649,7 +20975,8 @@ function registerTaskCommands(program2) {
20649
20975
  console.error(chalk3.red("--inbox needs an agent identity. Run `todos init <name>` or pass --agent <id>."));
20650
20976
  process.exit(1);
20651
20977
  }
20652
- filter["assigned_to"] = me.agent_id;
20978
+ assignedFilter = me.agent_id;
20979
+ assignedWasTyped = false;
20653
20980
  filter["not_created_by"] = me.agent_id;
20654
20981
  }
20655
20982
  if (opts.tags)
@@ -20665,8 +20992,11 @@ function registerTaskCommands(program2) {
20665
20992
  }
20666
20993
  }
20667
20994
  if (opts.agentName) {
20668
- filter["assigned_to"] = opts.agentName;
20995
+ assignedFilter = opts.agentName;
20996
+ assignedWasTyped = true;
20669
20997
  }
20998
+ if (assignedFilter !== undefined)
20999
+ filter["assigned_to"] = assignedFilter;
20670
21000
  if (opts.recurring)
20671
21001
  filter["has_recurrence"] = true;
20672
21002
  if (opts.limit !== undefined) {
@@ -20678,11 +21008,23 @@ function registerTaskCommands(program2) {
20678
21008
  }
20679
21009
  const creatorFilterActive = Boolean(filter["created_by"] || filter["not_created_by"]);
20680
21010
  const requestedLimit = filter["limit"];
20681
- const serverFilter = creatorFilterActive && cloud && requestedLimit !== undefined ? (() => {
20682
- const { limit: _dropped, ...rest } = filter;
20683
- return rest;
20684
- })() : filter;
21011
+ const reordersAfterQuery = Boolean(opts.sort);
21012
+ const narrowsAfterQuery = Boolean(opts.dueToday) || Boolean(opts.overdue) || creatorFilterActive && cloud;
21013
+ const withholdLimit = requestedLimit !== undefined && (reordersAfterQuery || narrowsAfterQuery);
21014
+ const scanCeiling = cloud && (withholdLimit || requestedLimit === undefined) ? Math.max(requestedLimit ?? 0, listScanLimit()) : undefined;
21015
+ const serverFilter = (() => {
21016
+ const base = withholdLimit ? (() => {
21017
+ const { limit: _dropped, ...rest } = filter;
21018
+ return rest;
21019
+ })() : filter;
21020
+ return scanCeiling === undefined ? base : { ...base, limit: scanCeiling };
21021
+ })();
20685
21022
  let tasks = cloud ? await cloudListTasks(cloud, serverFilter) : listTasks(serverFilter);
21023
+ if (scanCeiling !== undefined && tasks.length >= scanCeiling) {
21024
+ console.error(chalk3.yellow(`Warning: this query reached its ${scanCeiling}-row scan limit, so the rows below were
21025
+ ` + ` selected from a truncated set and may not be the true result.
21026
+ ` + ` Narrow it (--project, --status, --assigned) or raise TODOS_LIST_SCAN_LIMIT.`));
21027
+ }
20686
21028
  if (cloud && creatorFilterActive) {
20687
21029
  if (tasks.length > 0 && tasks.every((t) => !("created_by" in t))) {
20688
21030
  console.error(chalk3.yellow(`Warning: this server does not record task authorship, so the creator filter matched nothing to exclude.
@@ -20699,8 +21041,6 @@ function registerTaskCommands(program2) {
20699
21041
  return false;
20700
21042
  return true;
20701
21043
  });
20702
- if (requestedLimit !== undefined)
20703
- tasks = tasks.slice(0, requestedLimit);
20704
21044
  }
20705
21045
  if (opts.dueToday) {
20706
21046
  const todayEnd = new Date;
@@ -20725,6 +21065,21 @@ function registerTaskCommands(program2) {
20725
21065
  return 0;
20726
21066
  });
20727
21067
  }
21068
+ if (withholdLimit && requestedLimit !== undefined)
21069
+ tasks = tasks.slice(0, requestedLimit);
21070
+ if (assignedWasTyped && assignedFilter && tasks.length === 0) {
21071
+ try {
21072
+ const roster = await loadAssigneeContext(() => cloud ? cloudListAgents(cloud) : listAgents(), true);
21073
+ if (!roster.degraded) {
21074
+ const target = canonicalAgentRef(assignedFilter);
21075
+ const known = roster.agents.some((a) => canonicalAgentRef(a.name) === target || canonicalAgentRef(a.id) === target);
21076
+ if (!known) {
21077
+ console.error(chalk3.yellow(`Warning: no agent named '${assignedFilter}' is registered, so this empty result may be a
21078
+ ` + ` mistyped name rather than an empty queue. Check with 'todos agents'.`));
21079
+ }
21080
+ }
21081
+ } catch {}
21082
+ }
20728
21083
  const fmt = opts.format || (globalOpts.json ? "json" : "table");
20729
21084
  const outputTasks = redactBroadTasks(tasks);
20730
21085
  if (fmt === "json") {
@@ -21496,17 +21851,26 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21496
21851
  handleError(new Error("Task not found."));
21497
21852
  }
21498
21853
  });
21499
- program2.command("bulk <action> <ids...>").description("Bulk operation on multiple tasks (done, start, delete, plan/move-plan)").option("--plan <id>", "Plan ID for the plan/move-plan action").option("--clear-plan", "Remove plan assignment for the plan/move-plan action").action(async (action, ids, opts) => {
21854
+ program2.command("bulk <action> <ids...>").description("Bulk operation on multiple tasks (done, start, delete, plan/move-plan, tag, untag)").option("--plan <id>", "Plan ID for the plan/move-plan action").option("--clear-plan", "Remove plan assignment for the plan/move-plan action").option("--tag <tags>", "Comma-separated tags for the tag/untag action").option("--tags <tags>", "Comma-separated tags for the tag/untag action (alias for --tag)").action(async (action, ids, opts) => {
21500
21855
  const globalOpts = program2.opts();
21501
21856
  const results = [];
21502
21857
  const cloud = getTodosCloudClient();
21503
21858
  const isPlanAction = action === "plan" || action === "move-plan";
21859
+ const isTagAction = action === "tag" || action === "untag";
21504
21860
  if (isPlanAction && Boolean(opts.plan) === Boolean(opts.clearPlan)) {
21505
21861
  handleError(new Error("Use exactly one of --plan or --clear-plan with bulk plan."));
21506
21862
  }
21507
- const knownActions = new Set(["done", "complete", "start", "delete", "plan", "move-plan"]);
21863
+ const knownActions = new Set(["done", "complete", "start", "delete", "plan", "move-plan", "tag", "untag"]);
21508
21864
  if (!knownActions.has(action)) {
21509
- handleError(new Error(`Unknown action: ${action}. Use: done, start, delete, plan (alias: move-plan)`));
21865
+ handleError(new Error(`Unknown action: ${action}. Use: done, start, delete, plan (alias: move-plan), tag, untag`));
21866
+ }
21867
+ const tagArgument = resolveTagArgument(opts.tag, opts.tags);
21868
+ if (isTagAction && !tagArgument.ok) {
21869
+ handleError(new Error(`--tag and --tags name different tag sets (--tag "${tagArgument.conflict.tag}" vs --tags "${tagArgument.conflict.tags}"). ` + "They are aliases; pass one, or pass the same set to both."));
21870
+ }
21871
+ const bulkTags = isTagAction && tagArgument.ok ? parseTagList(tagArgument.raw) : [];
21872
+ if (isTagAction && bulkTags.length === 0) {
21873
+ handleError(new Error(`bulk ${action} needs --tag. Example: todos bulk ${action} <ids...> --tag directive:k_msd4cz8t_ste6f4`));
21510
21874
  }
21511
21875
  const bulkClaimAgentId = action === "start" ? resolveClaimIdentity("start", globalOpts.agent) : undefined;
21512
21876
  if (cloud) {
@@ -21535,6 +21899,14 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21535
21899
  await cloudTaskAction(cloud, resolvedId, "start", { agent_id: bulkClaimAgentId });
21536
21900
  } else if (action === "delete") {
21537
21901
  await cloudDeleteTask(cloud, resolvedId);
21902
+ } else if (isTagAction) {
21903
+ const current = await cloudGetTask(cloud, resolvedId);
21904
+ if (!current)
21905
+ throw new Error(`Task not found: ${rawId}`);
21906
+ const resolution = resolveBulkTags(current.tags, action, bulkTags);
21907
+ if (resolution.changed) {
21908
+ await cloudUpdateTask(cloud, resolvedId, { version: current.version, tags: resolution.tags });
21909
+ }
21538
21910
  } else {
21539
21911
  const current = await cloudGetTask(cloud, resolvedId);
21540
21912
  if (!current)
@@ -21571,6 +21943,16 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21571
21943
  } else if (action === "delete") {
21572
21944
  deleteTask(resolvedId);
21573
21945
  results.push({ id: resolvedId, success: true });
21946
+ } else if (isTagAction) {
21947
+ const current = getTask(resolvedId);
21948
+ if (!current) {
21949
+ throw new Error(`Task not found: ${rawId}`);
21950
+ }
21951
+ const resolution = resolveBulkTags(current.tags, action, bulkTags);
21952
+ if (resolution.changed) {
21953
+ updateTask(resolvedId, { version: current.version, tags: resolution.tags });
21954
+ }
21955
+ results.push({ id: resolvedId, success: true });
21574
21956
  } else if (isPlanAction) {
21575
21957
  const current = getTask(resolvedId);
21576
21958
  if (!current) {
@@ -21579,7 +21961,7 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21579
21961
  updateTask(resolvedId, { version: current.version, plan_id: planId });
21580
21962
  results.push({ id: resolvedId, success: true });
21581
21963
  } else {
21582
- handleError(new Error(`Unknown action: ${action}. Use: done, start, delete, plan (alias: move-plan)`));
21964
+ handleError(new Error(`Unknown action: ${action}. Use: done, start, delete, plan (alias: move-plan), tag, untag`));
21583
21965
  }
21584
21966
  } catch (e) {
21585
21967
  results.push({ id: rawId, success: false, error: e instanceof Error ? e.message : String(e) });
@@ -21597,6 +21979,7 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21597
21979
  }
21598
21980
  });
21599
21981
  }
21982
+ var DEFAULT_LIST_SCAN_LIMIT = 1e4;
21600
21983
  var init_task_commands = __esm(() => {
21601
21984
  init_database();
21602
21985
  init_projects();
@@ -21607,9 +21990,10 @@ var init_task_commands = __esm(() => {
21607
21990
  init_lock_display();
21608
21991
  init_claim_guard();
21609
21992
  init_assignee_guard();
21993
+ init_assignee_context();
21994
+ init_agents();
21610
21995
  init_helpers();
21611
21996
  init_output_redaction();
21612
- init_types();
21613
21997
  });
21614
21998
 
21615
21999
  // src/lib/plan-artifacts.ts
@@ -23441,7 +23825,8 @@ __export(exports_saved_search_views, {
23441
23825
  normalizeScope: () => normalizeScope,
23442
23826
  listSearchViews: () => listSearchViews,
23443
23827
  getSearchView: () => getSearchView,
23444
- deleteSearchView: () => deleteSearchView
23828
+ deleteSearchView: () => deleteSearchView,
23829
+ SAVED_SEARCH_SCOPES: () => SAVED_SEARCH_SCOPES
23445
23830
  });
23446
23831
  function parseFilters(value) {
23447
23832
  if (!value)
@@ -23461,10 +23846,8 @@ function rowToSavedSearchView(row) {
23461
23846
  };
23462
23847
  }
23463
23848
  function normalizeScope(scope) {
23464
- if (scope === "all" || scope === "tasks" || scope === "projects" || scope === "plans" || scope === "runs" || scope === "comments") {
23465
- return scope;
23466
- }
23467
- return "tasks";
23849
+ const candidate = (scope ?? "").trim().toLowerCase();
23850
+ return SAVED_SEARCH_SCOPES.includes(candidate) ? candidate : "tasks";
23468
23851
  }
23469
23852
  function normalizeName(name) {
23470
23853
  const normalized = name.trim();
@@ -23767,10 +24150,12 @@ function runSearchView(idOrName, db) {
23767
24150
  throw new Error(`Saved search view not found: ${idOrName}`);
23768
24151
  return { ...runSavedSearch(view.filters, view.scope, d), view };
23769
24152
  }
24153
+ var SAVED_SEARCH_SCOPES;
23770
24154
  var init_saved_search_views = __esm(() => {
23771
24155
  init_database();
23772
24156
  init_local_fields();
23773
24157
  init_search();
24158
+ SAVED_SEARCH_SCOPES = ["all", "tasks", "projects", "plans", "runs", "comments"];
23774
24159
  });
23775
24160
 
23776
24161
  // src/lib/claude-tasks.ts
@@ -26340,12 +26725,11 @@ function buildSearchFilters(query, opts, projectId) {
26340
26725
  const customFields = parseJsonObjectOption(opts.fieldCustom, "--field-custom");
26341
26726
  const labels = splitList(opts.fieldLabel);
26342
26727
  const tags = splitList(opts.tag);
26343
- const statuses = splitList(opts.status)?.map(normalizeStatus);
26344
26728
  const filters = {
26345
26729
  query: query || opts.query,
26346
26730
  project_id: opts.allProjects ? undefined : projectId,
26347
- status: statuses,
26348
- priority: splitList(opts.priority),
26731
+ status: parseEnumFlagList(opts.status, TASK_STATUS_FLAG),
26732
+ priority: parseEnumFlagList(opts.priority, TASK_PRIORITY_FLAG),
26349
26733
  assigned_to: opts.assigned,
26350
26734
  agent_id: opts.agentId,
26351
26735
  task_list_id: filterPatch?.task_list_id === undefined ? resolveTaskListFilter(opts.taskList, projectId) : undefined,
@@ -26450,7 +26834,7 @@ function registerProjectCommands(program2) {
26450
26834
  try {
26451
26835
  const cloud = getTodosCloudClient();
26452
26836
  const projectId = opts.allProjects ? undefined : cloud ? undefined : autoProject(globalOpts);
26453
- const scope = normalizeScope(opts.scope);
26837
+ const scope = parseEnumFlag(opts.scope, SEARCH_SCOPE_FLAG) ?? "tasks";
26454
26838
  const searchOpts = buildSearchFilters(query, opts, projectId);
26455
26839
  if (opts.saveAs) {
26456
26840
  const view = saveSearchView({
@@ -26542,7 +26926,7 @@ function registerProjectCommands(program2) {
26542
26926
  const view = saveSearchView({
26543
26927
  name,
26544
26928
  description: opts.description,
26545
- scope: normalizeScope(opts.scope),
26929
+ scope: parseEnumFlag(opts.scope, SEARCH_SCOPE_FLAG) ?? "tasks",
26546
26930
  filters: buildSearchFilters(opts.query, opts, projectId)
26547
26931
  });
26548
26932
  output(view, Boolean(globalOpts.json));
@@ -26555,7 +26939,7 @@ function registerProjectCommands(program2) {
26555
26939
  views.command("list").description("List local saved search views").option("--scope <scope>", "Filter by scope").action((opts) => {
26556
26940
  const globalOpts = program2.opts();
26557
26941
  try {
26558
- const rows = listSearchViews(opts.scope ? normalizeScope(opts.scope) : undefined);
26942
+ const rows = listSearchViews(parseEnumFlag(opts.scope, SEARCH_SCOPE_FLAG));
26559
26943
  output(rows, Boolean(globalOpts.json));
26560
26944
  if (!globalOpts.json) {
26561
26945
  if (rows.length === 0) {
@@ -26751,7 +27135,7 @@ function registerProjectCommands(program2) {
26751
27135
  const cloud = getTodosCloudClient();
26752
27136
  if (opts.show) {
26753
27137
  const project = cloud ? await cloudResolveProject(cloud, opts.show) : resolveExplicitProject(opts.show);
26754
- output(project, Boolean(globalOpts.json));
27138
+ outputRecord(project, Boolean(globalOpts.json), "Project:");
26755
27139
  return;
26756
27140
  }
26757
27141
  if (opts.update) {
@@ -26765,7 +27149,7 @@ function registerProjectCommands(program2) {
26765
27149
  }
26766
27150
  const current = cloud ? await cloudResolveProject(cloud, opts.update) : resolveExplicitProject(opts.update);
26767
27151
  const project = cloud ? await cloudUpdateProject(cloud, current.id, patch) : updateProject(current.id, patch);
26768
- output(project, Boolean(globalOpts.json));
27152
+ outputRecord(project, Boolean(globalOpts.json), "Project updated:");
26769
27153
  return;
26770
27154
  }
26771
27155
  if (opts.deregister) {
@@ -27073,6 +27457,7 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
27073
27457
  program2.command("export").description("Export tasks").option("-f, --format <format>", "Format: json, md, todos.md, or bridge", "json").option("-o, --output <path>", "Write export output to a file").option("--encrypt", "Encrypt bridge exports with a local encryption profile").option("--encryption-profile <name>", "Encryption profile name", "default").option("--allow-plaintext-sensitive", "Deprecated; bridge exports are redacted unless --encrypt is used").action(async (opts) => {
27074
27458
  const { listTasks: listTasks2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
27075
27459
  const globalOpts = program2.opts();
27460
+ opts.format = parseEnumFlag(opts.format, EXPORT_FORMAT_FLAG) ?? "json";
27076
27461
  const projectId = autoProject(globalOpts);
27077
27462
  const writeOutput = async (content) => {
27078
27463
  if (opts.output) {
@@ -27234,7 +27619,7 @@ function resolveTaskListId(partialId) {
27234
27619
  }
27235
27620
  return id;
27236
27621
  }
27237
- var CLOUD_GRAPH_EDGE_CONCURRENCY = 6;
27622
+ var SEARCH_SCOPE_FLAG, EXPORT_FORMATS, EXPORT_FORMAT_FLAG, CLOUD_GRAPH_EDGE_CONCURRENCY = 6;
27238
27623
  var init_project_commands = __esm(() => {
27239
27624
  init_database();
27240
27625
  init_projects();
@@ -27246,12 +27631,26 @@ var init_project_commands = __esm(() => {
27246
27631
  init_config();
27247
27632
  init_helpers();
27248
27633
  init_output_redaction();
27634
+ SEARCH_SCOPE_FLAG = {
27635
+ name: "--scope",
27636
+ vocabulary: SAVED_SEARCH_SCOPES,
27637
+ normalize: (value) => value.toLowerCase().trim(),
27638
+ allowList: false
27639
+ };
27640
+ EXPORT_FORMATS = ["json", "md", "markdown", "todos.md", "todos-md", "bridge"];
27641
+ EXPORT_FORMAT_FLAG = {
27642
+ name: "--format",
27643
+ vocabulary: EXPORT_FORMATS,
27644
+ normalize: (value) => value.toLowerCase().trim(),
27645
+ allowList: false
27646
+ };
27249
27647
  });
27250
27648
 
27251
27649
  // src/cli/commands/agent-commands.ts
27252
27650
  var exports_agent_commands = {};
27253
27651
  __export(exports_agent_commands, {
27254
- registerAgentCommands: () => registerAgentCommands
27652
+ registerAgentCommands: () => registerAgentCommands,
27653
+ findCaseVariantRows: () => findCaseVariantRows
27255
27654
  });
27256
27655
  import chalk6 from "chalk";
27257
27656
  import { execSync as execSync2 } from "child_process";
@@ -27265,6 +27664,15 @@ function resolveCloudAgentByNameOrId(agents, nameOrId) {
27265
27664
  return null;
27266
27665
  return matches.reduce((freshest, candidate) => new Date(candidate.last_seen_at).getTime() > new Date(freshest.last_seen_at).getTime() ? candidate : freshest);
27267
27666
  }
27667
+ function findCaseVariantRows(agents, requested) {
27668
+ const raw = requested.trim();
27669
+ const target = normalizeAgentNameInput(raw);
27670
+ if (!target)
27671
+ return [];
27672
+ if (agents.some((agent) => agent.name === raw))
27673
+ return [];
27674
+ return agents.filter((agent) => normalizeAgentNameInput(agent.name) === target);
27675
+ }
27268
27676
  function clearIdentityIfMine(agentId, agentName) {
27269
27677
  const persisted = readPersistedIdentity();
27270
27678
  if (!persisted)
@@ -27278,7 +27686,22 @@ function registerAgentCommands(program2) {
27278
27686
  const globalOpts = program2.opts();
27279
27687
  try {
27280
27688
  const cloud = getTodosCloudClient();
27281
- const result = cloud ? await cloudRegisterAgent(cloud, { name, description: opts.description }) : (await Promise.resolve().then(() => (init_agents(), exports_agents))).registerAgent({ name, description: opts.description });
27689
+ const registrationName = name.trim();
27690
+ if (cloud) {
27691
+ let roster = null;
27692
+ try {
27693
+ roster = await cloudListAgents(cloud);
27694
+ } catch {
27695
+ console.error(chalk6.yellow("Warning: could not read the shared roster, so the case-variant check was skipped for this registration."));
27696
+ }
27697
+ const variants = roster ? findCaseVariantRows(roster, registrationName) : [];
27698
+ if (variants.length > 0) {
27699
+ const listed = variants.map((a) => `${a.name} (${a.id})`).join(", ");
27700
+ console.error(chalk6.red(`'${name}' is a case variant of an agent that already exists: ${listed}. ` + `Agent names are ONE case-insensitive identity, so registering this spelling would create a second row ` + `and split the identity in two \u2014 tasks assigned to one spelling are invisible to the other. ` + `Register with the existing spelling instead.`));
27701
+ process.exit(1);
27702
+ }
27703
+ }
27704
+ const result = cloud ? await cloudRegisterAgent(cloud, { name: registrationName, description: opts.description }) : (await Promise.resolve().then(() => (init_agents(), exports_agents))).registerAgent({ name: registrationName, description: opts.description });
27282
27705
  const { isAgentConflict: isAgentConflict2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
27283
27706
  if (isAgentConflict2(result)) {
27284
27707
  console.error(chalk6.red("CONFLICT:"), result.message);
@@ -27623,7 +28046,7 @@ ${isOnline ? chalk6.green("\u25CF") : chalk6.dim("\u25CB")} ${chalk6.bold(agent.
27623
28046
  const list2 = cloud ? await cloudGetTaskList(cloud, resolved) : getTaskList(resolved);
27624
28047
  if (!list2)
27625
28048
  throw new Error(`Task list not found: ${ref}`);
27626
- output(list2, Boolean(globalOpts.json));
28049
+ outputRecord(list2, Boolean(globalOpts.json), "Task list:");
27627
28050
  return;
27628
28051
  }
27629
28052
  const patch = {
@@ -27634,7 +28057,7 @@ ${isOnline ? chalk6.green("\u25CF") : chalk6.dim("\u25CB")} ${chalk6.bold(agent.
27634
28057
  if (Object.keys(patch).length === 0)
27635
28058
  throw new Error("lists --update requires --name, --slug, or --description");
27636
28059
  const list = cloud ? await cloudUpdateTaskList(cloud, resolved, patch) : updateTaskList(resolved, patch);
27637
- output(list, Boolean(globalOpts.json));
28060
+ outputRecord(list, Boolean(globalOpts.json), "Task list updated:");
27638
28061
  return;
27639
28062
  }
27640
28063
  if (opts.delete) {
@@ -32185,9 +32608,11 @@ async function updateTask2(id, input, store) {
32185
32608
  throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
32186
32609
  }
32187
32610
  const reopened = existing.status === "completed" && input.status !== undefined && input.status !== "completed" && input.completed_at === undefined;
32611
+ const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
32188
32612
  const task = {
32189
32613
  ...existing,
32190
32614
  ...definedPatch(input),
32615
+ ...terminalNow ? { locked_by: null, locked_at: null } : {},
32191
32616
  version: existing.version + 1,
32192
32617
  updated_at: new Date().toISOString(),
32193
32618
  tags: input.tags ?? existing.tags,
@@ -34528,8 +34953,19 @@ function handleStats(_ctx, json2) {
34528
34953
  recurring_tasks: countRecurringTasks()
34529
34954
  });
34530
34955
  }
34956
+ function taskStatusQueryParam(url) {
34957
+ const raw = url.searchParams.get("status");
34958
+ if (!raw)
34959
+ return { ok: true, value: undefined };
34960
+ const result = resolveEnumVocabulary(raw, { name: "status", vocabulary: TASK_STATUSES });
34961
+ if (!result.ok)
34962
+ return { ok: false, message: result.message };
34963
+ return { ok: true, value: collapseEnumValues(result.values) };
34964
+ }
34531
34965
  async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
34532
- const status = url.searchParams.get("status") || undefined;
34966
+ const statusParam = taskStatusQueryParam(url);
34967
+ if (!statusParam.ok)
34968
+ return json2({ error: statusParam.message }, 400);
34533
34969
  const projectId = url.searchParams.get("project_id") || undefined;
34534
34970
  const sessionId = url.searchParams.get("session_id") || undefined;
34535
34971
  const agentId = url.searchParams.get("agent_id") || undefined;
@@ -34537,7 +34973,7 @@ async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
34537
34973
  const offsetParam = url.searchParams.get("offset");
34538
34974
  const fields = parseFieldsParam(url);
34539
34975
  const tasks = listTasks({
34540
- status,
34976
+ status: statusParam.value,
34541
34977
  project_id: projectId,
34542
34978
  session_id: sessionId,
34543
34979
  agent_id: agentId,
@@ -34602,9 +35038,19 @@ async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
34602
35038
  }
34603
35039
  function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
34604
35040
  const format = url.searchParams.get("format") || "json";
34605
- const status = url.searchParams.get("status") || undefined;
35041
+ const statusParam = taskStatusQueryParam(url);
35042
+ if (!statusParam.ok) {
35043
+ return new Response(JSON.stringify({ error: statusParam.message }), {
35044
+ status: 400,
35045
+ headers: { "Content-Type": "application/json" }
35046
+ });
35047
+ }
34606
35048
  const projectId = url.searchParams.get("project_id") || undefined;
34607
- const tasks = listTasks({ status, project_id: projectId, limit: 1e4 });
35049
+ const tasks = listTasks({
35050
+ status: statusParam.value,
35051
+ project_id: projectId,
35052
+ limit: 1e4
35053
+ });
34608
35054
  const summaries = tasks.map((t) => taskToSummary2(t));
34609
35055
  if (format === "csv") {
34610
35056
  const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
@@ -35186,6 +35632,7 @@ var init_routes = __esm(() => {
35186
35632
  init_tasks();
35187
35633
  init_database();
35188
35634
  init_types();
35635
+ init_types();
35189
35636
  init_projects();
35190
35637
  init_agents();
35191
35638
  init_plans();
@@ -37070,8 +37517,38 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
37070
37517
  operationId: "listTasks",
37071
37518
  summary: "List tasks",
37072
37519
  parameters: [
37073
- { name: "status", in: "query", schema: { type: "string" } },
37074
- { name: "priority", in: "query", schema: { type: "string" } },
37520
+ {
37521
+ name: "status",
37522
+ in: "query",
37523
+ description: `Task status, or a comma-separated list of statuses. Allowed values: ${TASK_STATUSES.join(", ")}.`,
37524
+ style: "form",
37525
+ explode: false,
37526
+ schema: {
37527
+ oneOf: [
37528
+ { type: "string", enum: [...TASK_STATUSES] },
37529
+ {
37530
+ type: "array",
37531
+ items: { type: "string", enum: [...TASK_STATUSES] }
37532
+ }
37533
+ ]
37534
+ }
37535
+ },
37536
+ {
37537
+ name: "priority",
37538
+ in: "query",
37539
+ description: `Task priority, or a comma-separated list of priorities. Allowed values: ${TASK_PRIORITIES.join(", ")}.`,
37540
+ style: "form",
37541
+ explode: false,
37542
+ schema: {
37543
+ oneOf: [
37544
+ { type: "string", enum: [...TASK_PRIORITIES] },
37545
+ {
37546
+ type: "array",
37547
+ items: { type: "string", enum: [...TASK_PRIORITIES] }
37548
+ }
37549
+ ]
37550
+ }
37551
+ },
37075
37552
  { name: "project_id", in: "query", schema: { type: "string" } },
37076
37553
  { name: "parent_id", in: "query", schema: { type: "string", nullable: true } },
37077
37554
  { name: "include_subtasks", in: "query", schema: { type: "boolean" } },
@@ -37583,6 +38060,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
37583
38060
  var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
37584
38061
  var init_openapi = __esm(() => {
37585
38062
  init_package_version();
38063
+ init_types();
37586
38064
  taskSchema = {
37587
38065
  type: "object",
37588
38066
  properties: {
@@ -37850,6 +38328,15 @@ function json3(body, status = 200) {
37850
38328
  function error(status, message, extra) {
37851
38329
  return json3({ error: message, ...extra ?? {} }, status);
37852
38330
  }
38331
+ function enumQueryParam(url, name, vocabulary) {
38332
+ const raw = url.searchParams.get(name);
38333
+ if (raw === null || raw === "")
38334
+ return { ok: true, value: undefined };
38335
+ const result = resolveEnumVocabulary(raw, { name, vocabulary });
38336
+ if (!result.ok)
38337
+ return { ok: false, response: error(400, result.message) };
38338
+ return { ok: true, value: collapseEnumValues(result.values) };
38339
+ }
37853
38340
  function validateTaskCompletion(value) {
37854
38341
  if (!value || typeof value !== "object" || Array.isArray(value))
37855
38342
  return { ok: false, message: "completion body must be an object" };
@@ -38270,14 +38757,16 @@ async function handleV1Request(req, url, dependencies = {}) {
38270
38757
  return error(400, "include_subtasks must be true or false");
38271
38758
  }
38272
38759
  const hasParentFilter = url.searchParams.has("parent_id");
38760
+ const statusParam = enumQueryParam(url, "status", TASK_STATUSES);
38761
+ if (!statusParam.ok)
38762
+ return statusParam.response;
38763
+ const priorityParam = enumQueryParam(url, "priority", TASK_PRIORITIES);
38764
+ if (!priorityParam.ok)
38765
+ return priorityParam.response;
38273
38766
  const filter = {
38274
38767
  ...url.searchParams.get("q") ? { query: url.searchParams.get("q") } : {},
38275
- ...url.searchParams.get("status") ? {
38276
- status: url.searchParams.get("status").includes(",") ? url.searchParams.get("status").split(",") : url.searchParams.get("status")
38277
- } : {},
38278
- ...url.searchParams.get("priority") ? {
38279
- priority: url.searchParams.get("priority").includes(",") ? url.searchParams.get("priority").split(",") : url.searchParams.get("priority")
38280
- } : {},
38768
+ ...statusParam.value !== undefined ? { status: statusParam.value } : {},
38769
+ ...priorityParam.value !== undefined ? { priority: priorityParam.value } : {},
38281
38770
  ...url.searchParams.get("project_id") ? { project_id: url.searchParams.get("project_id") } : {},
38282
38771
  ...hasParentFilter ? { parent_id: url.searchParams.get("parent_id") || null, include_subtasks: true } : includeSubtasks !== null ? { include_subtasks: includeSubtasks === "true" } : {},
38283
38772
  ...url.searchParams.get("plan_id") ? { plan_id: url.searchParams.get("plan_id") } : {},
@@ -38911,7 +39400,13 @@ async function handleV1Request(req, url, dependencies = {}) {
38911
39400
  return error(405, `method ${method} not allowed on /v1/refs/:ref`);
38912
39401
  if (!store.gitRefs)
38913
39402
  return error(501, "git ref links are not supported by this storage backend");
38914
- const refs = await store.gitRefs.find(id);
39403
+ let decodedRef;
39404
+ try {
39405
+ decodedRef = decodeURIComponent(id);
39406
+ } catch {
39407
+ return error(400, "ref path segment has invalid percent encoding");
39408
+ }
39409
+ const refs = await store.gitRefs.find(decodedRef);
38915
39410
  return json3({ refs, count: refs.length });
38916
39411
  }
38917
39412
  if (resource === "next" && !id) {
@@ -43509,7 +44004,7 @@ async function validateMcpAssignee(value, allowSeat) {
43509
44004
  return verdict.assignee;
43510
44005
  }
43511
44006
  function registerTaskCrudTools(server, ctx) {
43512
- const { shouldRegisterTool, resolveId, formatError, formatTask } = ctx;
44007
+ const { shouldRegisterTool, resolveId, formatError, formatTask, applyFocus } = ctx;
43513
44008
  function mutationTaskResponse(task) {
43514
44009
  const compact = compactTask(task, 240);
43515
44010
  compact["version"] = task.version;
@@ -43585,6 +44080,7 @@ function registerTaskCrudTools(server, ctx) {
43585
44080
  payload.max_retries = retry_count;
43586
44081
  if (deadline)
43587
44082
  payload.due_at = deadline;
44083
+ applyFocus(payload, router.agent_id || undefined);
43588
44084
  const created = await cloudCreateTask(cloud, payload);
43589
44085
  return { content: [{ type: "text", text: mutationTaskResponse(created) }] };
43590
44086
  }
@@ -43611,6 +44107,7 @@ function registerTaskCrudTools(server, ctx) {
43611
44107
  resolved.max_retries = retry_count;
43612
44108
  if (deadline)
43613
44109
  resolved.due_at = deadline;
44110
+ applyFocus(resolved, router.agent_id || undefined);
43614
44111
  const task = createTask(resolved);
43615
44112
  return { content: [{ type: "text", text: mutationTaskResponse(task) }] };
43616
44113
  } catch (e) {
@@ -50421,10 +50918,23 @@ function registerTaskAutoTools(server, ctx) {
50421
50918
  return { content: [{ type: "text", text: "No active agents available for rebalancing." }] };
50422
50919
  const activeTasks = listTasks4({ project_id: resolvedProjectId, status: ["pending", "in_progress"], limit: 1000 }, undefined);
50423
50920
  const agentKeyByAlias = new Map;
50921
+ const ambiguousAgentAliases = new Set;
50922
+ const indexAgentAlias = (alias, agentId) => {
50923
+ const normalizedAlias2 = alias.toLowerCase();
50924
+ if (ambiguousAgentAliases.has(normalizedAlias2))
50925
+ return;
50926
+ const existingAgentId = agentKeyByAlias.get(normalizedAlias2);
50927
+ if (existingAgentId !== undefined && existingAgentId !== agentId) {
50928
+ agentKeyByAlias.delete(normalizedAlias2);
50929
+ ambiguousAgentAliases.add(normalizedAlias2);
50930
+ return;
50931
+ }
50932
+ agentKeyByAlias.set(normalizedAlias2, agentId);
50933
+ };
50424
50934
  for (const agent of agents) {
50425
- agentKeyByAlias.set(String(agent.id).toLowerCase(), agent.id);
50935
+ indexAgentAlias(String(agent.id), agent.id);
50426
50936
  if (agent.name)
50427
- agentKeyByAlias.set(String(agent.name).toLowerCase(), agent.id);
50937
+ indexAgentAlias(String(agent.name), agent.id);
50428
50938
  }
50429
50939
  const canonicalAgentKey = (assignedTo) => assignedTo ? agentKeyByAlias.get(assignedTo.toLowerCase()) : undefined;
50430
50940
  const load = new Map(agents.map((agent) => [agent.id, activeTasks.filter((t) => canonicalAgentKey(t.assigned_to) === agent.id).length]));
@@ -61810,9 +62320,16 @@ ${text2}` }] };
61810
62320
  return { content: [{ type: "text", text: `Agent not found: ${id || name}` }], isError: true };
61811
62321
  }
61812
62322
  const oldName = agent.name;
61813
- const updated = updateAgent(agent.id, { name: new_name });
61814
62323
  const db = getDatabase();
61815
- const tasksResult = db.run("UPDATE tasks SET assigned_to = ? WHERE LOWER(assigned_to) = LOWER(?)", [new_name, oldName]);
62324
+ let oldNameUniquelyIdentifiesAgent = false;
62325
+ try {
62326
+ oldNameUniquelyIdentifiesAgent = resolvePartialId(db, "agents", oldName) === agent.id;
62327
+ } catch (error2) {
62328
+ if (!(error2 instanceof IdentityAliasAmbiguousError))
62329
+ throw error2;
62330
+ }
62331
+ const updated = updateAgent(agent.id, { name: new_name });
62332
+ const tasksResult = db.run(oldNameUniquelyIdentifiesAgent ? "UPDATE tasks SET assigned_to = ? WHERE LOWER(assigned_to) = LOWER(?)" : "UPDATE tasks SET assigned_to = ? WHERE assigned_to = ?", [new_name, oldName]);
61816
62333
  const taskNote = tasksResult.changes > 0 ? `
61817
62334
  Updated assigned_to on ${tasksResult.changes} task(s).` : "";
61818
62335
  return {
@@ -61996,6 +62513,7 @@ var init_agents2 = __esm(() => {
61996
62513
  init_agents();
61997
62514
  init_config();
61998
62515
  init_database();
62516
+ init_types();
61999
62517
  init_cloud_router();
62000
62518
  });
62001
62519
 
@@ -62828,7 +63346,8 @@ function buildServer() {
62828
63346
  formatError,
62829
63347
  formatTask,
62830
63348
  formatTaskDetail,
62831
- getAgentFocus
63349
+ getAgentFocus,
63350
+ applyFocus
62832
63351
  };
62833
63352
  registerTaskCrudTools(server, toolContext);
62834
63353
  registerTaskProjectTools(server, toolContext);
@@ -66275,7 +66794,7 @@ ${error2.message}
66275
66794
  const globalOpts = program2.opts();
66276
66795
  const projectId = autoProject(globalOpts);
66277
66796
  const interval = parseInt(opts.interval, 10) * 1000;
66278
- const statusFilter = opts.status ? opts.status.split(",").map((s) => normalizeStatus(s.trim())) : ["pending", "in_progress"];
66797
+ const statusFilter = parseEnumFlagList(opts.status, TASK_STATUS_FLAG) ?? ["pending", "in_progress"];
66279
66798
  function render2() {
66280
66799
  const tasks = listTasks({ project_id: projectId, status: statusFilter });
66281
66800
  const all = listTasks({ project_id: projectId });
@@ -68882,8 +69401,8 @@ Findings`));
68882
69401
  const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
68883
69402
  const { statSync: statSync12 } = await import("fs");
68884
69403
  const { join: join27 } = await import("path");
68885
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
68886
- const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join27(home, ".hasna", "todos", "todos.db");
69404
+ const { getHomeDir: getHomeDir2 } = await Promise.resolve().then(() => (init_sync_utils(), exports_sync_utils));
69405
+ const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join27(getHomeDir2(), ".hasna", "todos", "todos.db");
68887
69406
  let size = "unknown";
68888
69407
  try {
68889
69408
  size = `${(statSync12(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
@@ -72048,7 +72567,8 @@ var init_mcp_hooks_commands = __esm(() => {
72048
72567
  init_tasks();
72049
72568
  init_helpers();
72050
72569
  init_cloud_router();
72051
- HOME2 = process.env["HOME"] || process.env["USERPROFILE"] || "~";
72570
+ init_sync_utils();
72571
+ HOME2 = getHomeDir();
72052
72572
  });
72053
72573
 
72054
72574
  // src/cli/commands/dispatch.tsx
@@ -72161,7 +72681,11 @@ function registerDispatchCommands(program2) {
72161
72681
  process.exit(1);
72162
72682
  }
72163
72683
  }
72164
- const statuses = opts.status ? opts.status.split(",").map((s) => s.trim()) : undefined;
72684
+ const statuses = parseEnumFlagList(opts.status, {
72685
+ name: "--status",
72686
+ vocabulary: DISPATCH_STATUSES,
72687
+ normalize: (value) => value.toLowerCase().trim()
72688
+ });
72165
72689
  const dispatches = listDispatches({ status: statuses, limit: opts.limit ?? 20 });
72166
72690
  if (useJson) {
72167
72691
  console.log(JSON.stringify(dispatches, null, 2));
@@ -72194,6 +72718,8 @@ var init_dispatch3 = __esm(() => {
72194
72718
  init_dispatch();
72195
72719
  init_dispatch_formatter();
72196
72720
  init_tmux();
72721
+ init_types();
72722
+ init_helpers();
72197
72723
  });
72198
72724
 
72199
72725
  // src/cli/commands/machines.tsx