@hasna/todos 0.14.0 → 0.15.2

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.14.0",
2126
+ version: "0.15.2",
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",
@@ -2444,11 +2444,30 @@ var init_types = __esm(() => {
2444
2444
  });
2445
2445
 
2446
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
+ });
2447
2465
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
2448
2466
  import { createHash } from "crypto";
2467
+ import { homedir } from "os";
2449
2468
  import { join } from "path";
2450
2469
  function getHomeDir() {
2451
- return process.env["HOME"] || process.env["USERPROFILE"] || "~";
2470
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
2452
2471
  }
2453
2472
  function getTodosGlobalDir() {
2454
2473
  return join(getHomeDir(), ".hasna", "todos");
@@ -2536,7 +2555,7 @@ function hasSyncFingerprintChanged(record) {
2536
2555
  }
2537
2556
  var TODO_SYNC_FINGERPRINT_KEY = "todos_sync_fingerprint", HOME;
2538
2557
  var init_sync_utils = __esm(() => {
2539
- HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
2558
+ HOME = getHomeDir();
2540
2559
  });
2541
2560
 
2542
2561
  // src/lib/config.ts
@@ -6414,6 +6433,15 @@ function hasOption(args, option) {
6414
6433
  function positionalArgs(args) {
6415
6434
  return args.filter((arg) => !arg.startsWith("-"));
6416
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
+ }
6417
6445
  function isReadOnlyConfigInvocation(invocation) {
6418
6446
  if (invocation.command !== "config")
6419
6447
  return false;
@@ -6434,6 +6462,8 @@ function isMetadataInvocation(args, invocation) {
6434
6462
  return true;
6435
6463
  if (invocation.command === "manual" && invocation.commandArgs.length === 0)
6436
6464
  return true;
6465
+ if (isBundledStaticInvocation(invocation))
6466
+ return true;
6437
6467
  if (invocation.command === "help" && invocation.commandArgs.every((arg) => !arg.startsWith("-")))
6438
6468
  return true;
6439
6469
  if (invocation.command === "config") {
@@ -6451,6 +6481,14 @@ function firstPresentOption(args, candidates) {
6451
6481
  function disqualifyingArgument(invocation) {
6452
6482
  const command = invocation.command;
6453
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
+ }
6454
6492
  switch (command) {
6455
6493
  case "task":
6456
6494
  return positionalArgs(args)[0] === "upsert" ? null : { blame: "any subcommand other than `upsert`", remedy: "use `todos task upsert`" };
@@ -6478,13 +6516,22 @@ function disqualifyingArgument(invocation) {
6478
6516
  const action = positionalArgs(args)[0];
6479
6517
  if (action === "plan" || action === "move-plan")
6480
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
+ }
6481
6528
  if (!action) {
6482
- 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" };
6483
6530
  }
6484
6531
  if (!["done", "complete", "start", "delete"].includes(action)) {
6485
6532
  return {
6486
6533
  blame: `the \`${action}\` action`,
6487
- 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"
6488
6535
  };
6489
6536
  }
6490
6537
  return firstPresentOption(args, ["--plan", "--clear-plan"]);
@@ -6541,7 +6588,8 @@ function assertRemoteCommandSupported(invocation) {
6541
6588
  }
6542
6589
  if (!command || !commandSupportsRemote(invocation)) {
6543
6590
  const blame = command ? disqualifyingArgument(invocation) : null;
6544
- 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`;
6545
6593
  throw new Error(`REMOTE_COMMAND_UNSUPPORTED: ${detail}`);
6546
6594
  }
6547
6595
  }
@@ -6561,7 +6609,7 @@ function initializeTodosCliAuthority(args = process.argv.slice(2), env = process
6561
6609
  }
6562
6610
  return { route: "remote-http", v1_base_url: client.baseUrl };
6563
6611
  }
6564
- 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" });
6565
6613
  var init_stage_a = __esm(() => {
6566
6614
  init_esm();
6567
6615
  init_cloud_router();
@@ -6751,7 +6799,28 @@ var init_stage_a = __esm(() => {
6751
6799
  reliability: ["scorecards"],
6752
6800
  lists: ["task-lists", "tl"]
6753
6801
  };
6754
- 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
+ ]);
6755
6824
  REMOTE_COMMANDS = new Set([
6756
6825
  "active",
6757
6826
  "add",
@@ -10773,8 +10842,7 @@ function findGitRoot(startDir) {
10773
10842
  return null;
10774
10843
  }
10775
10844
  function getGlobalDbPath() {
10776
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
10777
- return join3(home, ".hasna", "todos", "todos.db");
10845
+ return join3(getHomeDir(), ".hasna", "todos", "todos.db");
10778
10846
  }
10779
10847
  function hasExplicitProjectArg(args = process.argv.slice(2)) {
10780
10848
  return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
@@ -10996,6 +11064,7 @@ var init_database = __esm(() => {
10996
11064
  init_machines();
10997
11065
  init_identity_mapping();
10998
11066
  init_types();
11067
+ init_sync_utils();
10999
11068
  ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
11000
11069
  });
11001
11070
 
@@ -11502,14 +11571,21 @@ function suggestVocabularyMatches(value, vocabulary, limit = 3) {
11502
11571
  }
11503
11572
  function resolveEnumVocabulary(raw, spec) {
11504
11573
  const allowList = spec.allowList !== false;
11505
- const rawElements = (allowList ? raw.split(",") : [raw]).map((element) => element.trim()).filter((element) => element.length > 0);
11506
- if (rawElements.length === 0) {
11574
+ const rawElements = (allowList ? raw.split(",") : [raw]).map((element) => element.trim());
11575
+ if (rawElements.every((element) => element.length === 0)) {
11507
11576
  return {
11508
11577
  ok: false,
11509
11578
  message: `${spec.name} requires a value. Allowed values: ${spec.vocabulary.join(", ")}.`,
11510
11579
  invalid: []
11511
11580
  };
11512
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
+ }
11513
11589
  const normalized = rawElements.map((element) => spec.normalize ? spec.normalize(element) : element);
11514
11590
  const allowed = new Set(spec.vocabulary);
11515
11591
  const invalid2 = [];
@@ -12707,7 +12783,7 @@ var init_event_hooks = __esm(() => {
12707
12783
  // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
12708
12784
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
12709
12785
  import { existsSync as existsSync6 } from "fs";
12710
- import { homedir } from "os";
12786
+ import { homedir as homedir2 } from "os";
12711
12787
  import { join as join5 } from "path";
12712
12788
  import { createHmac, timingSafeEqual } from "crypto";
12713
12789
  import { randomUUID as randomUUID2 } from "crypto";
@@ -12809,7 +12885,7 @@ function channelMatchesEvent(channel, event) {
12809
12885
  return channel.filters.some((filter) => eventMatchesFilter(event, filter));
12810
12886
  }
12811
12887
  function getEventsDataDir(override) {
12812
- 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");
12813
12889
  }
12814
12890
 
12815
12891
  class JsonEventsStore {
@@ -19241,6 +19317,49 @@ var init_tasks = __esm(() => {
19241
19317
  init_calendar();
19242
19318
  });
19243
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
+
19244
19363
  // src/cli/claim-guard.ts
19245
19364
  function resolveClaimIdentity(verb, explicit) {
19246
19365
  const resolved = resolveWritableIdentity(explicit);
@@ -19261,10 +19380,10 @@ function normalizeAgentNameInput(name) {
19261
19380
 
19262
19381
  // src/lib/assignee-validation.ts
19263
19382
  import { readFileSync as readFileSync4 } from "fs";
19264
- import { homedir as homedir2 } from "os";
19383
+ import { homedir as homedir3 } from "os";
19265
19384
  import { join as join8 } from "path";
19266
19385
  function defaultSeatRosterPath() {
19267
- 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");
19268
19387
  }
19269
19388
  function loadSeatSlugs(path = defaultSeatRosterPath()) {
19270
19389
  try {
@@ -20398,11 +20517,31 @@ function resolveProjectIdOrSlug(input) {
20398
20517
  return row.id;
20399
20518
  handleError(new Error(`Project not found: ${input}`));
20400
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
+ }
20401
20533
  function parseStatus(value) {
20402
20534
  if (!value)
20403
20535
  return;
20404
20536
  return parseEnumFlagList(value, { ...TASK_STATUS_FLAG, allowList: false })?.[0];
20405
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;
20544
+ }
20406
20545
  function parseIntOption(value, flag) {
20407
20546
  if (value === undefined)
20408
20547
  return;
@@ -20540,11 +20679,10 @@ function computeLocalReparent(current, opts) {
20540
20679
  return patch;
20541
20680
  }
20542
20681
  function registerTaskCommands(program2) {
20543
- 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) => {
20544
20683
  const globalOpts = program2.opts();
20545
20684
  opts.tags = opts.tags || opts.tag;
20546
20685
  opts.list = opts.list || opts.taskList;
20547
- const creator = resolveCreatorIdentity(opts.createdBy || globalOpts.agent);
20548
20686
  const router = resolveWritableIdentity(opts.createdBy || globalOpts.agent);
20549
20687
  const requestedAssign = opts.assign ? await resolveValidatedAssignee(opts.assign, Boolean(opts.assignSeat), (v) => `--assign ${v} --assign-seat`) : undefined;
20550
20688
  const assignee = requestedAssign || (opts.unassigned ? undefined : router.agent_id || undefined);
@@ -20555,8 +20693,9 @@ function registerTaskCommands(program2) {
20555
20693
  if (cloud) {
20556
20694
  let task3;
20557
20695
  try {
20558
- const cloudProjectRef = opts.project || globalOpts.project;
20696
+ const cloudProjectRef = projectRefFromOpts(opts, globalOpts);
20559
20697
  const cloudProjectId = cloudProjectRef ? await cloudResolveProjectRef(cloud, cloudProjectRef) : undefined;
20698
+ warnMissingProject(cloudProjectId, projectOptOut(opts));
20560
20699
  const cloudTaskListId = opts.list ? await cloudResolveTaskListRef(cloud, opts.list, cloudProjectId) : undefined;
20561
20700
  if (opts.list && !cloudTaskListId) {
20562
20701
  throw new Error(`Could not resolve task list ID or slug: ${opts.list}`);
@@ -20576,9 +20715,10 @@ function registerTaskCommands(program2) {
20576
20715
  status: parseStatus(opts.status),
20577
20716
  task_list_id: cloudTaskListId,
20578
20717
  agent_id: globalOpts.agent || router.agent_id || undefined,
20579
- created_by: creator.agent_id || undefined,
20718
+ created_by: router.agent_id || undefined,
20580
20719
  session_id: globalOpts.session,
20581
20720
  project_id: cloudProjectId,
20721
+ working_dir: process.cwd(),
20582
20722
  estimated_minutes: opts.estimated !== undefined ? parseIntOption(opts.estimated, "--estimated") : undefined,
20583
20723
  sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseIntOption(opts.slaMinutes ?? opts.sla, "--sla-minutes") : undefined,
20584
20724
  requires_approval: opts.approval || undefined,
@@ -20597,8 +20737,9 @@ function registerTaskCommands(program2) {
20597
20737
  }
20598
20738
  return;
20599
20739
  }
20600
- const explicitProject = opts.project || globalOpts.project;
20601
- const projectId = explicitProject ? resolveProjectIdOrSlug(explicitProject) : autoProject(globalOpts);
20740
+ const explicitProject = projectRefFromOpts(opts, globalOpts);
20741
+ const projectId = explicitProject ? resolveProjectIdOrSlug(explicitProject) : projectOptOut(opts) ? undefined : autoProject(globalOpts);
20742
+ warnMissingProject(projectId, projectOptOut(opts));
20602
20743
  opts.tags = opts.tags || opts.tag;
20603
20744
  opts.list = opts.list || opts.taskList;
20604
20745
  const taskListId = opts.list ? (() => {
@@ -20622,7 +20763,7 @@ function registerTaskCommands(program2) {
20622
20763
  status: parseStatus(opts.status),
20623
20764
  task_list_id: taskListId,
20624
20765
  agent_id: globalOpts.agent || router.agent_id || undefined,
20625
- created_by: creator.agent_id || undefined,
20766
+ created_by: router.agent_id || undefined,
20626
20767
  session_id: globalOpts.session,
20627
20768
  project_id: projectId,
20628
20769
  working_dir: process.cwd(),
@@ -20810,15 +20951,19 @@ function registerTaskCommands(program2) {
20810
20951
  }
20811
20952
  filter["task_list_id"] = listId;
20812
20953
  }
20813
- if (opts.status) {
20954
+ if (opts.status !== undefined) {
20814
20955
  filter["status"] = parseEnumFlag(opts.status, TASK_STATUS_FLAG);
20815
20956
  } else if (!opts.all) {
20816
20957
  filter["status"] = ["pending", "in_progress"];
20817
20958
  }
20818
- if (opts.priority)
20959
+ if (opts.priority !== undefined)
20819
20960
  filter["priority"] = parseEnumFlag(opts.priority, TASK_PRIORITY_FLAG);
20820
- if (opts.assigned)
20821
- filter["assigned_to"] = opts.assigned;
20961
+ let assignedFilter;
20962
+ let assignedWasTyped = false;
20963
+ if (opts.assigned) {
20964
+ assignedFilter = opts.assigned;
20965
+ assignedWasTyped = true;
20966
+ }
20822
20967
  if (opts.createdBy)
20823
20968
  filter["created_by"] = canonicalAgentRef(opts.createdBy);
20824
20969
  if (opts.notCreatedBy)
@@ -20829,7 +20974,8 @@ function registerTaskCommands(program2) {
20829
20974
  console.error(chalk3.red("--inbox needs an agent identity. Run `todos init <name>` or pass --agent <id>."));
20830
20975
  process.exit(1);
20831
20976
  }
20832
- filter["assigned_to"] = me.agent_id;
20977
+ assignedFilter = me.agent_id;
20978
+ assignedWasTyped = false;
20833
20979
  filter["not_created_by"] = me.agent_id;
20834
20980
  }
20835
20981
  if (opts.tags)
@@ -20845,8 +20991,11 @@ function registerTaskCommands(program2) {
20845
20991
  }
20846
20992
  }
20847
20993
  if (opts.agentName) {
20848
- filter["assigned_to"] = opts.agentName;
20994
+ assignedFilter = opts.agentName;
20995
+ assignedWasTyped = true;
20849
20996
  }
20997
+ if (assignedFilter !== undefined)
20998
+ filter["assigned_to"] = assignedFilter;
20850
20999
  if (opts.recurring)
20851
21000
  filter["has_recurrence"] = true;
20852
21001
  if (opts.limit !== undefined) {
@@ -20858,11 +21007,23 @@ function registerTaskCommands(program2) {
20858
21007
  }
20859
21008
  const creatorFilterActive = Boolean(filter["created_by"] || filter["not_created_by"]);
20860
21009
  const requestedLimit = filter["limit"];
20861
- const serverFilter = creatorFilterActive && cloud && requestedLimit !== undefined ? (() => {
20862
- const { limit: _dropped, ...rest } = filter;
20863
- return rest;
20864
- })() : filter;
21010
+ const reordersAfterQuery = Boolean(opts.sort);
21011
+ const narrowsAfterQuery = Boolean(opts.dueToday) || Boolean(opts.overdue) || creatorFilterActive && cloud;
21012
+ const withholdLimit = requestedLimit !== undefined && (reordersAfterQuery || narrowsAfterQuery);
21013
+ const scanCeiling = cloud && (withholdLimit || requestedLimit === undefined) ? Math.max(requestedLimit ?? 0, listScanLimit()) : undefined;
21014
+ const serverFilter = (() => {
21015
+ const base = withholdLimit ? (() => {
21016
+ const { limit: _dropped, ...rest } = filter;
21017
+ return rest;
21018
+ })() : filter;
21019
+ return scanCeiling === undefined ? base : { ...base, limit: scanCeiling };
21020
+ })();
20865
21021
  let tasks = cloud ? await cloudListTasks(cloud, serverFilter) : listTasks(serverFilter);
21022
+ if (scanCeiling !== undefined && tasks.length >= scanCeiling) {
21023
+ console.error(chalk3.yellow(`Warning: this query reached its ${scanCeiling}-row scan limit, so the rows below were
21024
+ ` + ` selected from a truncated set and may not be the true result.
21025
+ ` + ` Narrow it (--project, --status, --assigned) or raise TODOS_LIST_SCAN_LIMIT.`));
21026
+ }
20866
21027
  if (cloud && creatorFilterActive) {
20867
21028
  if (tasks.length > 0 && tasks.every((t) => !("created_by" in t))) {
20868
21029
  console.error(chalk3.yellow(`Warning: this server does not record task authorship, so the creator filter matched nothing to exclude.
@@ -20879,8 +21040,6 @@ function registerTaskCommands(program2) {
20879
21040
  return false;
20880
21041
  return true;
20881
21042
  });
20882
- if (requestedLimit !== undefined)
20883
- tasks = tasks.slice(0, requestedLimit);
20884
21043
  }
20885
21044
  if (opts.dueToday) {
20886
21045
  const todayEnd = new Date;
@@ -20905,6 +21064,21 @@ function registerTaskCommands(program2) {
20905
21064
  return 0;
20906
21065
  });
20907
21066
  }
21067
+ if (withholdLimit && requestedLimit !== undefined)
21068
+ tasks = tasks.slice(0, requestedLimit);
21069
+ if (assignedWasTyped && assignedFilter && tasks.length === 0) {
21070
+ try {
21071
+ const roster = await loadAssigneeContext(() => cloud ? cloudListAgents(cloud) : listAgents(), true);
21072
+ if (!roster.degraded) {
21073
+ const target = canonicalAgentRef(assignedFilter);
21074
+ const known = roster.agents.some((a) => canonicalAgentRef(a.name) === target || canonicalAgentRef(a.id) === target);
21075
+ if (!known) {
21076
+ console.error(chalk3.yellow(`Warning: no agent named '${assignedFilter}' is registered, so this empty result may be a
21077
+ ` + ` mistyped name rather than an empty queue. Check with 'todos agents'.`));
21078
+ }
21079
+ }
21080
+ } catch {}
21081
+ }
20908
21082
  const fmt = opts.format || (globalOpts.json ? "json" : "table");
20909
21083
  const outputTasks = redactBroadTasks(tasks);
20910
21084
  if (fmt === "json") {
@@ -21676,17 +21850,26 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21676
21850
  handleError(new Error("Task not found."));
21677
21851
  }
21678
21852
  });
21679
- 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) => {
21853
+ 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) => {
21680
21854
  const globalOpts = program2.opts();
21681
21855
  const results = [];
21682
21856
  const cloud = getTodosCloudClient();
21683
21857
  const isPlanAction = action === "plan" || action === "move-plan";
21858
+ const isTagAction = action === "tag" || action === "untag";
21684
21859
  if (isPlanAction && Boolean(opts.plan) === Boolean(opts.clearPlan)) {
21685
21860
  handleError(new Error("Use exactly one of --plan or --clear-plan with bulk plan."));
21686
21861
  }
21687
- const knownActions = new Set(["done", "complete", "start", "delete", "plan", "move-plan"]);
21862
+ const knownActions = new Set(["done", "complete", "start", "delete", "plan", "move-plan", "tag", "untag"]);
21688
21863
  if (!knownActions.has(action)) {
21689
- handleError(new Error(`Unknown action: ${action}. Use: done, start, delete, plan (alias: move-plan)`));
21864
+ handleError(new Error(`Unknown action: ${action}. Use: done, start, delete, plan (alias: move-plan), tag, untag`));
21865
+ }
21866
+ const tagArgument = resolveTagArgument(opts.tag, opts.tags);
21867
+ if (isTagAction && !tagArgument.ok) {
21868
+ 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."));
21869
+ }
21870
+ const bulkTags = isTagAction && tagArgument.ok ? parseTagList(tagArgument.raw) : [];
21871
+ if (isTagAction && bulkTags.length === 0) {
21872
+ handleError(new Error(`bulk ${action} needs --tag. Example: todos bulk ${action} <ids...> --tag directive:k_msd4cz8t_ste6f4`));
21690
21873
  }
21691
21874
  const bulkClaimAgentId = action === "start" ? resolveClaimIdentity("start", globalOpts.agent) : undefined;
21692
21875
  if (cloud) {
@@ -21715,6 +21898,14 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21715
21898
  await cloudTaskAction(cloud, resolvedId, "start", { agent_id: bulkClaimAgentId });
21716
21899
  } else if (action === "delete") {
21717
21900
  await cloudDeleteTask(cloud, resolvedId);
21901
+ } else if (isTagAction) {
21902
+ const current = await cloudGetTask(cloud, resolvedId);
21903
+ if (!current)
21904
+ throw new Error(`Task not found: ${rawId}`);
21905
+ const resolution = resolveBulkTags(current.tags, action, bulkTags);
21906
+ if (resolution.changed) {
21907
+ await cloudUpdateTask(cloud, resolvedId, { version: current.version, tags: resolution.tags });
21908
+ }
21718
21909
  } else {
21719
21910
  const current = await cloudGetTask(cloud, resolvedId);
21720
21911
  if (!current)
@@ -21751,6 +21942,16 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21751
21942
  } else if (action === "delete") {
21752
21943
  deleteTask(resolvedId);
21753
21944
  results.push({ id: resolvedId, success: true });
21945
+ } else if (isTagAction) {
21946
+ const current = getTask(resolvedId);
21947
+ if (!current) {
21948
+ throw new Error(`Task not found: ${rawId}`);
21949
+ }
21950
+ const resolution = resolveBulkTags(current.tags, action, bulkTags);
21951
+ if (resolution.changed) {
21952
+ updateTask(resolvedId, { version: current.version, tags: resolution.tags });
21953
+ }
21954
+ results.push({ id: resolvedId, success: true });
21754
21955
  } else if (isPlanAction) {
21755
21956
  const current = getTask(resolvedId);
21756
21957
  if (!current) {
@@ -21759,7 +21960,7 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21759
21960
  updateTask(resolvedId, { version: current.version, plan_id: planId });
21760
21961
  results.push({ id: resolvedId, success: true });
21761
21962
  } else {
21762
- handleError(new Error(`Unknown action: ${action}. Use: done, start, delete, plan (alias: move-plan)`));
21963
+ handleError(new Error(`Unknown action: ${action}. Use: done, start, delete, plan (alias: move-plan), tag, untag`));
21763
21964
  }
21764
21965
  } catch (e) {
21765
21966
  results.push({ id: rawId, success: false, error: e instanceof Error ? e.message : String(e) });
@@ -21777,6 +21978,7 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21777
21978
  }
21778
21979
  });
21779
21980
  }
21981
+ var DEFAULT_LIST_SCAN_LIMIT = 1e4;
21780
21982
  var init_task_commands = __esm(() => {
21781
21983
  init_database();
21782
21984
  init_projects();
@@ -21787,6 +21989,8 @@ var init_task_commands = __esm(() => {
21787
21989
  init_lock_display();
21788
21990
  init_claim_guard();
21789
21991
  init_assignee_guard();
21992
+ init_assignee_context();
21993
+ init_agents();
21790
21994
  init_helpers();
21791
21995
  init_output_redaction();
21792
21996
  });
@@ -39195,7 +39399,13 @@ async function handleV1Request(req, url, dependencies = {}) {
39195
39399
  return error(405, `method ${method} not allowed on /v1/refs/:ref`);
39196
39400
  if (!store.gitRefs)
39197
39401
  return error(501, "git ref links are not supported by this storage backend");
39198
- const refs = await store.gitRefs.find(id);
39402
+ let decodedRef;
39403
+ try {
39404
+ decodedRef = decodeURIComponent(id);
39405
+ } catch {
39406
+ return error(400, "ref path segment has invalid percent encoding");
39407
+ }
39408
+ const refs = await store.gitRefs.find(decodedRef);
39199
39409
  return json3({ refs, count: refs.length });
39200
39410
  }
39201
39411
  if (resource === "next" && !id) {
@@ -43793,7 +44003,7 @@ async function validateMcpAssignee(value, allowSeat) {
43793
44003
  return verdict.assignee;
43794
44004
  }
43795
44005
  function registerTaskCrudTools(server, ctx) {
43796
- const { shouldRegisterTool, resolveId, formatError, formatTask } = ctx;
44006
+ const { shouldRegisterTool, resolveId, formatError, formatTask, applyFocus } = ctx;
43797
44007
  function mutationTaskResponse(task) {
43798
44008
  const compact = compactTask(task, 240);
43799
44009
  compact["version"] = task.version;
@@ -43826,7 +44036,7 @@ function registerTaskCrudTools(server, ctx) {
43826
44036
  project_id: exports_external.string().optional().describe("Project ID"),
43827
44037
  task_list_id: exports_external.string().optional().describe("Task list ID"),
43828
44038
  assigned_to: exports_external.string().optional().describe("Agent ID or name to assign to"),
43829
- created_by: exports_external.string().optional().describe("Agent who FILED this task. Defaults to the ambient agent identity (todos init / TODOS_AGENT_ID)."),
44039
+ created_by: exports_external.string().optional().describe("Agent who FILED this task. Defaults to the PROCESS-BOUND ambient identity (--agent / TODOS_AGENT_ID) if set; the machine-shared identity `todos init` persists to disk is never used here, because it names the station, not the caller."),
43830
44040
  unassigned: exports_external.boolean().optional().describe("Deliberately file with no assignee. Without it, the task defaults to the filer."),
43831
44041
  allow_seat: exports_external.boolean().optional().describe("Allow assigned_to to name a durable seat. A seat queue has no session watching it, so this must be deliberate."),
43832
44042
  depends_on: exports_external.array(exports_external.string()).optional().describe("Array of task IDs this task depends on"),
@@ -43841,14 +44051,13 @@ function registerTaskCrudTools(server, ctx) {
43841
44051
  try {
43842
44052
  const { depends_on, assigned_to, project_id, task_list_id, tags, estimate, confidence, retry_count, deadline, created_by, unassigned, allow_seat, ...rest } = params;
43843
44053
  const requestedAssignee = assigned_to ? await validateMcpAssignee(assigned_to, Boolean(allow_seat)) : undefined;
43844
- const creator = resolveCreatorIdentity(created_by);
43845
44054
  const router = resolveWritableIdentity(created_by);
43846
44055
  const assignee = requestedAssignee || (unassigned ? undefined : router.agent_id || undefined);
43847
44056
  const cloud = getTodosCloudClient();
43848
44057
  if (cloud) {
43849
44058
  const payload = { ...rest };
43850
- if (creator.agent_id)
43851
- payload.created_by = creator.agent_id;
44059
+ if (router.agent_id)
44060
+ payload.created_by = router.agent_id;
43852
44061
  if (router.agent_id)
43853
44062
  payload.agent_id = payload.agent_id ?? router.agent_id;
43854
44063
  if (assignee)
@@ -43869,12 +44078,13 @@ function registerTaskCrudTools(server, ctx) {
43869
44078
  payload.max_retries = retry_count;
43870
44079
  if (deadline)
43871
44080
  payload.due_at = deadline;
44081
+ applyFocus(payload, router.agent_id || undefined);
43872
44082
  const created = await cloudCreateTask(cloud, payload);
43873
44083
  return { content: [{ type: "text", text: mutationTaskResponse(created) }] };
43874
44084
  }
43875
44085
  const resolved = { ...rest };
43876
- if (creator.agent_id)
43877
- resolved.created_by = creator.agent_id;
44086
+ if (router.agent_id)
44087
+ resolved.created_by = router.agent_id;
43878
44088
  if (router.agent_id)
43879
44089
  resolved.agent_id = resolved.agent_id ?? router.agent_id;
43880
44090
  if (assignee)
@@ -43895,6 +44105,7 @@ function registerTaskCrudTools(server, ctx) {
43895
44105
  resolved.max_retries = retry_count;
43896
44106
  if (deadline)
43897
44107
  resolved.due_at = deadline;
44108
+ applyFocus(resolved, router.agent_id || undefined);
43898
44109
  const task = createTask(resolved);
43899
44110
  return { content: [{ type: "text", text: mutationTaskResponse(task) }] };
43900
44111
  } catch (e) {
@@ -63133,7 +63344,8 @@ function buildServer() {
63133
63344
  formatError,
63134
63345
  formatTask,
63135
63346
  formatTaskDetail,
63136
- getAgentFocus
63347
+ getAgentFocus,
63348
+ applyFocus
63137
63349
  };
63138
63350
  registerTaskCrudTools(server, toolContext);
63139
63351
  registerTaskProjectTools(server, toolContext);
@@ -66580,7 +66792,7 @@ ${error2.message}
66580
66792
  const globalOpts = program2.opts();
66581
66793
  const projectId = autoProject(globalOpts);
66582
66794
  const interval = parseInt(opts.interval, 10) * 1000;
66583
- const statusFilter = opts.status ? opts.status.split(",").map((s) => normalizeStatus(s.trim())) : ["pending", "in_progress"];
66795
+ const statusFilter = parseEnumFlagList(opts.status, TASK_STATUS_FLAG) ?? ["pending", "in_progress"];
66584
66796
  function render2() {
66585
66797
  const tasks = listTasks({ project_id: projectId, status: statusFilter });
66586
66798
  const all = listTasks({ project_id: projectId });
@@ -69187,8 +69399,8 @@ Findings`));
69187
69399
  const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
69188
69400
  const { statSync: statSync12 } = await import("fs");
69189
69401
  const { join: join27 } = await import("path");
69190
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
69191
- const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join27(home, ".hasna", "todos", "todos.db");
69402
+ const { getHomeDir: getHomeDir2 } = await Promise.resolve().then(() => (init_sync_utils(), exports_sync_utils));
69403
+ const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join27(getHomeDir2(), ".hasna", "todos", "todos.db");
69192
69404
  let size = "unknown";
69193
69405
  try {
69194
69406
  size = `${(statSync12(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
@@ -72353,7 +72565,8 @@ var init_mcp_hooks_commands = __esm(() => {
72353
72565
  init_tasks();
72354
72566
  init_helpers();
72355
72567
  init_cloud_router();
72356
- HOME2 = process.env["HOME"] || process.env["USERPROFILE"] || "~";
72568
+ init_sync_utils();
72569
+ HOME2 = getHomeDir();
72357
72570
  });
72358
72571
 
72359
72572
  // src/cli/commands/dispatch.tsx