@hasna/todos 0.11.61 → 0.11.63

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/README.md CHANGED
@@ -425,19 +425,23 @@ todos webhooks add loops \
425
425
  --arg=todos-task \
426
426
  --arg=--provider \
427
427
  --arg=codewith \
428
- --arg=--auth-profile \
429
- --arg=account005 \
428
+ --arg=--auth-profile-pool \
429
+ --arg=account004,account005,account006 \
430
430
  --arg=--permission-mode \
431
431
  --arg=bypass \
432
432
  --arg=--sandbox \
433
433
  --arg=danger-full-access \
434
+ --arg=--worktree-mode \
435
+ --arg=required \
434
436
  --timeout-ms 900000 \
435
437
  --json
436
438
  ```
437
439
 
438
440
  When a task is created, `@hasna/events` sends the event JSON on stdin and in
439
441
  `HASNA_EVENT_JSON`. OpenLoops uses that event to create a deduped one-shot
440
- worker/verifier workflow for the task. The event data includes task identity,
442
+ worker/verifier workflow for the task. Use account-profile pools instead of a
443
+ single pinned profile, and require isolated worktrees for repo-mutating routes.
444
+ The event data includes task identity,
441
445
  title, description, project/list ids, working directory, tags, metadata, status,
442
446
  priority, approval state, and timestamps. Event metadata includes routing-safe
443
447
  project/list/path fields, `route_enabled` when the task metadata opts in, and an
@@ -1 +1 @@
1
- {"version":3,"file":"project-commands.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/project-commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoFzC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAqzBvD"}
1
+ {"version":3,"file":"project-commands.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/project-commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkIzC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAqzBvD"}
package/dist/cli/index.js CHANGED
@@ -16631,6 +16631,42 @@ function parseJsonObjectOption(value, label) {
16631
16631
  } catch {}
16632
16632
  throw new Error(`${label} must be a JSON object`);
16633
16633
  }
16634
+ function resolveTaskListFilter(input, projectId) {
16635
+ if (!input)
16636
+ return;
16637
+ const db = getDatabase();
16638
+ const id = input.length >= 36 ? db.query("SELECT id FROM task_lists WHERE id = ?").get(input) : null;
16639
+ if (id)
16640
+ return id.id;
16641
+ if (input.length < 36) {
16642
+ const partialIds = db.query("SELECT id FROM task_lists WHERE id LIKE ?").all(`${input}%`);
16643
+ if (partialIds.length === 1)
16644
+ return partialIds[0].id;
16645
+ if (partialIds.length > 1) {
16646
+ throw new Error(`Ambiguous task list ID: ${input}`);
16647
+ }
16648
+ }
16649
+ if (projectId) {
16650
+ const projectSlug = db.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ?").get(projectId, input);
16651
+ if (projectSlug)
16652
+ return projectSlug.id;
16653
+ const standaloneSlug = db.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(input);
16654
+ if (standaloneSlug)
16655
+ return standaloneSlug.id;
16656
+ const otherProjectSlugs = db.query("SELECT id FROM task_lists WHERE slug = ? LIMIT 2").all(input);
16657
+ if (otherProjectSlugs.length > 0) {
16658
+ throw new Error(`Task list slug "${input}" does not belong to the selected project`);
16659
+ }
16660
+ } else {
16661
+ const slugMatches = db.query("SELECT id FROM task_lists WHERE slug = ?").all(input);
16662
+ if (slugMatches.length === 1)
16663
+ return slugMatches[0].id;
16664
+ if (slugMatches.length > 1) {
16665
+ throw new Error(`Ambiguous task list slug: ${input}. Use --project or the task list ID.`);
16666
+ }
16667
+ }
16668
+ throw new Error(`Could not resolve task list ID: ${input}`);
16669
+ }
16634
16670
  function buildSearchFilters(query, opts, projectId) {
16635
16671
  const filterPatch = parseJsonObjectOption(opts.filter, "--filter");
16636
16672
  const customFields = parseJsonObjectOption(opts.fieldCustom, "--field-custom");
@@ -16644,7 +16680,7 @@ function buildSearchFilters(query, opts, projectId) {
16644
16680
  priority: splitList(opts.priority),
16645
16681
  assigned_to: opts.assigned,
16646
16682
  agent_id: opts.agentId,
16647
- task_list_id: opts.taskList,
16683
+ task_list_id: filterPatch?.task_list_id === undefined ? resolveTaskListFilter(opts.taskList, projectId) : undefined,
16648
16684
  plan_id: opts.plan,
16649
16685
  task_id: opts.task,
16650
16686
  tags,
@@ -26382,7 +26418,7 @@ var init_dispatch = __esm(() => {
26382
26418
  // src/mcp/tools/dispatch.ts
26383
26419
  function registerDispatchTools(server, { shouldRegisterTool, resolveId, formatError }) {
26384
26420
  if (shouldRegisterTool("dispatch_tasks")) {
26385
- server.tool("dispatch_tasks", "Send specific tasks to a tmux window. Formats a message from the given task IDs and dispatches it with a configurable delay before hitting Enter. Supports dry-run preview and scheduled sending.", {
26421
+ server.tool("dispatch_tasks", "Legacy/emergency only: send specific tasks to a tmux window after a human explicitly chooses tmux delivery. Normal pending-task routing should use task-created headless workflows. Supports dry-run preview and scheduled sending.", {
26386
26422
  task_ids: exports_external.array(exports_external.string()).min(1).describe("IDs of the tasks to dispatch"),
26387
26423
  target: exports_external.string().describe("tmux target \u2014 window name, session:window, or session:window.pane"),
26388
26424
  delay_ms: exports_external.number().optional().describe("Delay in ms between sending the message and hitting Enter. Auto-calculated from message length (3-5s) if omitted."),
@@ -26425,7 +26461,7 @@ ${message}`
26425
26461
  });
26426
26462
  }
26427
26463
  if (shouldRegisterTool("dispatch_task_list")) {
26428
- server.tool("dispatch_task_list", "Send all tasks from a task list to a tmux window. Fetches matching tasks, formats them as a grouped message with a list header, and dispatches. Supports status filtering, dry-run, and scheduled sending.", {
26464
+ server.tool("dispatch_task_list", "Legacy/emergency only: send all tasks from a task list to a tmux window after a human explicitly chooses tmux delivery. Normal pending-task routing should use task-created headless workflows. Supports filtering, dry-run, and scheduled sending.", {
26429
26465
  task_list_id: exports_external.string().describe("ID or slug of the task list to dispatch"),
26430
26466
  target: exports_external.string().describe("tmux target \u2014 window name, session:window, or session:window.pane"),
26431
26467
  filter_status: exports_external.array(exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"])).optional().describe("Only include tasks with these statuses. Default: pending."),
@@ -26474,7 +26510,7 @@ ${message}`
26474
26510
  });
26475
26511
  }
26476
26512
  if (shouldRegisterTool("dispatch_to_multiple")) {
26477
- server.tool("dispatch_to_multiple", "Fan-out: send the same tasks or task list to multiple tmux windows in sequence. A stagger delay is applied between each window.", {
26513
+ server.tool("dispatch_to_multiple", "Legacy/emergency fan-out: send the same tasks or task list to multiple tmux windows in sequence after a human explicitly chooses tmux delivery. A stagger delay is applied between each window.", {
26478
26514
  targets: exports_external.array(exports_external.string()).min(2).describe("Array of tmux targets to dispatch to"),
26479
26515
  task_ids: exports_external.array(exports_external.string()).optional().describe("Task IDs to dispatch (use this or task_list_id)"),
26480
26516
  task_list_id: exports_external.string().optional().describe("Task list ID to dispatch (use this or task_ids)"),
@@ -26532,7 +26568,7 @@ ${lines.join(`
26532
26568
  });
26533
26569
  }
26534
26570
  if (shouldRegisterTool("run_due_dispatches")) {
26535
- server.tool("run_due_dispatches", "Manually trigger all pending dispatches that are due (scheduled_at <= now). Returns the count fired.", {
26571
+ server.tool("run_due_dispatches", "Legacy/emergency only: manually trigger all pending tmux dispatches that are due (scheduled_at <= now). Returns the count fired.", {
26536
26572
  dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false."),
26537
26573
  confirm_busy: exports_external.boolean().optional().describe("Send even if target tmux panes appear busy. Default: false."),
26538
26574
  all: exports_external.boolean().optional().describe("Ignore scheduled_at and fire all pending dispatches immediately.")
package/dist/index.js CHANGED
@@ -29862,7 +29862,7 @@ var AGENT_ADAPTER_DOCS = {
29862
29862
  ],
29863
29863
  examples: [
29864
29864
  { title: "Manual Takumi MCP add", command: "takumi mcp add --scope project todos -- todos-mcp" },
29865
- { title: "Dispatch to tmux pane", command: "todos dispatch agents:0 --tasks <id>" },
29865
+ { title: "Legacy tmux handoff", command: "todos dispatch agents:0 --tasks <id>" },
29866
29866
  { title: "Handoff on session end", command: "todos goal handoff release-v2 --format md --agent takumi-agent" }
29867
29867
  ]
29868
29868
  }
@@ -2,7 +2,8 @@ import type { Database } from "bun:sqlite";
2
2
  import type { CreateDispatchInput, Dispatch } from "../types/index.js";
3
3
  export type { FormatOpts } from "./dispatch-formatter.js";
4
4
  /**
5
- * Execute a single dispatch: resolve tasks, format, send to tmux, update status.
5
+ * Legacy/emergency path: resolve tasks, format, send to tmux, update status.
6
+ * Default pending-task routing should use task-created headless workflows.
6
7
  */
7
8
  export declare function executeDispatch(dispatch: Dispatch, opts?: {
8
9
  dryRun?: boolean;
@@ -18,7 +19,7 @@ export declare function runDueDispatches(opts?: {
18
19
  confirmBusy?: boolean;
19
20
  }, db?: Database): Promise<number>;
20
21
  /**
21
- * Fan-out: dispatch the same task set to multiple tmux windows.
22
+ * Legacy/emergency fan-out: dispatch the same task set to multiple tmux windows.
22
23
  * Returns the list of created Dispatch objects.
23
24
  */
24
25
  export declare function dispatchToMultiple(input: Omit<CreateDispatchInput, "target_window"> & {
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/lib/dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAW3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAGvE,YAAY,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D;;GAEG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,QAAQ,EAClB,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,yBAAyB,EAAE,UAAU,CAAA;CAAO,EACjH,EAAE,CAAC,EAAE,QAAQ,GACZ,OAAO,CAAC,IAAI,CAAC,CA0Df;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,EACtD,EAAE,CAAC,EAAE,QAAQ,GACZ,OAAO,CAAC,MAAM,CAAC,CAejB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,IAAI,CAAC,mBAAmB,EAAE,eAAe,CAAC,GAAG;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,EAC9F,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,EACtD,EAAE,CAAC,EAAE,QAAQ,GACZ,OAAO,CAAC,QAAQ,EAAE,CAAC,CAmBrB"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/lib/dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAW3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAGvE,YAAY,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D;;;GAGG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,QAAQ,EAClB,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,yBAAyB,EAAE,UAAU,CAAA;CAAO,EACjH,EAAE,CAAC,EAAE,QAAQ,GACZ,OAAO,CAAC,IAAI,CAAC,CA0Df;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,EACtD,EAAE,CAAC,EAAE,QAAQ,GACZ,OAAO,CAAC,MAAM,CAAC,CAejB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,IAAI,CAAC,mBAAmB,EAAE,eAAe,CAAC,GAAG;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,EAC9F,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,EACtD,EAAE,CAAC,EAAE,QAAQ,GACZ,OAAO,CAAC,QAAQ,EAAE,CAAC,CAmBrB"}
package/dist/mcp/index.js CHANGED
@@ -14839,7 +14839,7 @@ var init_dispatch = __esm(() => {
14839
14839
  // src/mcp/tools/dispatch.ts
14840
14840
  function registerDispatchTools(server, { shouldRegisterTool, resolveId, formatError }) {
14841
14841
  if (shouldRegisterTool("dispatch_tasks")) {
14842
- server.tool("dispatch_tasks", "Send specific tasks to a tmux window. Formats a message from the given task IDs and dispatches it with a configurable delay before hitting Enter. Supports dry-run preview and scheduled sending.", {
14842
+ server.tool("dispatch_tasks", "Legacy/emergency only: send specific tasks to a tmux window after a human explicitly chooses tmux delivery. Normal pending-task routing should use task-created headless workflows. Supports dry-run preview and scheduled sending.", {
14843
14843
  task_ids: exports_external.array(exports_external.string()).min(1).describe("IDs of the tasks to dispatch"),
14844
14844
  target: exports_external.string().describe("tmux target \u2014 window name, session:window, or session:window.pane"),
14845
14845
  delay_ms: exports_external.number().optional().describe("Delay in ms between sending the message and hitting Enter. Auto-calculated from message length (3-5s) if omitted."),
@@ -14882,7 +14882,7 @@ ${message}`
14882
14882
  });
14883
14883
  }
14884
14884
  if (shouldRegisterTool("dispatch_task_list")) {
14885
- server.tool("dispatch_task_list", "Send all tasks from a task list to a tmux window. Fetches matching tasks, formats them as a grouped message with a list header, and dispatches. Supports status filtering, dry-run, and scheduled sending.", {
14885
+ server.tool("dispatch_task_list", "Legacy/emergency only: send all tasks from a task list to a tmux window after a human explicitly chooses tmux delivery. Normal pending-task routing should use task-created headless workflows. Supports filtering, dry-run, and scheduled sending.", {
14886
14886
  task_list_id: exports_external.string().describe("ID or slug of the task list to dispatch"),
14887
14887
  target: exports_external.string().describe("tmux target \u2014 window name, session:window, or session:window.pane"),
14888
14888
  filter_status: exports_external.array(exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"])).optional().describe("Only include tasks with these statuses. Default: pending."),
@@ -14931,7 +14931,7 @@ ${message}`
14931
14931
  });
14932
14932
  }
14933
14933
  if (shouldRegisterTool("dispatch_to_multiple")) {
14934
- server.tool("dispatch_to_multiple", "Fan-out: send the same tasks or task list to multiple tmux windows in sequence. A stagger delay is applied between each window.", {
14934
+ server.tool("dispatch_to_multiple", "Legacy/emergency fan-out: send the same tasks or task list to multiple tmux windows in sequence after a human explicitly chooses tmux delivery. A stagger delay is applied between each window.", {
14935
14935
  targets: exports_external.array(exports_external.string()).min(2).describe("Array of tmux targets to dispatch to"),
14936
14936
  task_ids: exports_external.array(exports_external.string()).optional().describe("Task IDs to dispatch (use this or task_list_id)"),
14937
14937
  task_list_id: exports_external.string().optional().describe("Task list ID to dispatch (use this or task_ids)"),
@@ -14989,7 +14989,7 @@ ${lines.join(`
14989
14989
  });
14990
14990
  }
14991
14991
  if (shouldRegisterTool("run_due_dispatches")) {
14992
- server.tool("run_due_dispatches", "Manually trigger all pending dispatches that are due (scheduled_at <= now). Returns the count fired.", {
14992
+ server.tool("run_due_dispatches", "Legacy/emergency only: manually trigger all pending tmux dispatches that are due (scheduled_at <= now). Returns the count fired.", {
14993
14993
  dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false."),
14994
14994
  confirm_busy: exports_external.boolean().optional().describe("Send even if target tmux panes appear busy. Default: false."),
14995
14995
  all: exports_external.boolean().optional().describe("Ignore scheduled_at and fire all pending dispatches immediately.")
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "packageName": "@hasna/todos",
3
- "packageVersion": "0.11.61",
3
+ "packageVersion": "0.11.63",
4
4
  "repository": "https://github.com/hasna/todos.git",
5
- "gitCommit": "7dced09f93029fe58db334ff712e10094beefae2",
6
- "generatedAt": "2026-06-27T14:41:53.504Z"
5
+ "gitCommit": "86965960de6c58207876ec5f096f1cbc53f3122c",
6
+ "generatedAt": "2026-06-28T08:42:09.113Z"
7
7
  }
@@ -32524,7 +32524,7 @@ var init_dispatch = __esm(() => {
32524
32524
  // src/mcp/tools/dispatch.ts
32525
32525
  function registerDispatchTools(server, { shouldRegisterTool, resolveId, formatError: formatError2 }) {
32526
32526
  if (shouldRegisterTool("dispatch_tasks")) {
32527
- server.tool("dispatch_tasks", "Send specific tasks to a tmux window. Formats a message from the given task IDs and dispatches it with a configurable delay before hitting Enter. Supports dry-run preview and scheduled sending.", {
32527
+ server.tool("dispatch_tasks", "Legacy/emergency only: send specific tasks to a tmux window after a human explicitly chooses tmux delivery. Normal pending-task routing should use task-created headless workflows. Supports dry-run preview and scheduled sending.", {
32528
32528
  task_ids: exports_external.array(exports_external.string()).min(1).describe("IDs of the tasks to dispatch"),
32529
32529
  target: exports_external.string().describe("tmux target \u2014 window name, session:window, or session:window.pane"),
32530
32530
  delay_ms: exports_external.number().optional().describe("Delay in ms between sending the message and hitting Enter. Auto-calculated from message length (3-5s) if omitted."),
@@ -32567,7 +32567,7 @@ ${message}`
32567
32567
  });
32568
32568
  }
32569
32569
  if (shouldRegisterTool("dispatch_task_list")) {
32570
- server.tool("dispatch_task_list", "Send all tasks from a task list to a tmux window. Fetches matching tasks, formats them as a grouped message with a list header, and dispatches. Supports status filtering, dry-run, and scheduled sending.", {
32570
+ server.tool("dispatch_task_list", "Legacy/emergency only: send all tasks from a task list to a tmux window after a human explicitly chooses tmux delivery. Normal pending-task routing should use task-created headless workflows. Supports filtering, dry-run, and scheduled sending.", {
32571
32571
  task_list_id: exports_external.string().describe("ID or slug of the task list to dispatch"),
32572
32572
  target: exports_external.string().describe("tmux target \u2014 window name, session:window, or session:window.pane"),
32573
32573
  filter_status: exports_external.array(exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"])).optional().describe("Only include tasks with these statuses. Default: pending."),
@@ -32616,7 +32616,7 @@ ${message}`
32616
32616
  });
32617
32617
  }
32618
32618
  if (shouldRegisterTool("dispatch_to_multiple")) {
32619
- server.tool("dispatch_to_multiple", "Fan-out: send the same tasks or task list to multiple tmux windows in sequence. A stagger delay is applied between each window.", {
32619
+ server.tool("dispatch_to_multiple", "Legacy/emergency fan-out: send the same tasks or task list to multiple tmux windows in sequence after a human explicitly chooses tmux delivery. A stagger delay is applied between each window.", {
32620
32620
  targets: exports_external.array(exports_external.string()).min(2).describe("Array of tmux targets to dispatch to"),
32621
32621
  task_ids: exports_external.array(exports_external.string()).optional().describe("Task IDs to dispatch (use this or task_list_id)"),
32622
32622
  task_list_id: exports_external.string().optional().describe("Task list ID to dispatch (use this or task_ids)"),
@@ -32674,7 +32674,7 @@ ${lines.join(`
32674
32674
  });
32675
32675
  }
32676
32676
  if (shouldRegisterTool("run_due_dispatches")) {
32677
- server.tool("run_due_dispatches", "Manually trigger all pending dispatches that are due (scheduled_at <= now). Returns the count fired.", {
32677
+ server.tool("run_due_dispatches", "Legacy/emergency only: manually trigger all pending tmux dispatches that are due (scheduled_at <= now). Returns the count fired.", {
32678
32678
  dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false."),
32679
32679
  confirm_busy: exports_external.boolean().optional().describe("Send even if target tmux panes appear busy. Default: false."),
32680
32680
  all: exports_external.boolean().optional().describe("Ignore scheduled_at and fire all pending dispatches immediately.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.11.61",
3
+ "version": "0.11.63",
4
4
  "description": "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",