Skip to content

Add tags filter to task_list (MCP + REST + domain)#3

Open
memphyssk wants to merge 1 commit into
keshrath:mainfrom
memphyssk:feat/task-list-tags-filter
Open

Add tags filter to task_list (MCP + REST + domain)#3
memphyssk wants to merge 1 commit into
keshrath:mainfrom
memphyssk:feat/task-list-tags-filter

Conversation

@memphyssk
Copy link
Copy Markdown

What

Adds a tags filter to task_list across all three transports (MCP, REST, domain). Currently only the write side (task_create / task_update) accepts tags — task_list drops them on the floor. This enables cross-project tag rollups (e.g., OKR or iteration progress) without forcing every caller to do per-project list + client-side filter + union.

Why

We're using agent-tasks as the queue engine for a multi-project framework where tasks are tagged with okr:OKR-H1-3 and iter:H1 to support cross-project rollups (a single OKR's progress spans both engineering and business projects, each with its own pipeline). The natural query is task_list({ tags: ["okr:OKR-H1-3"] }). Today this requires:

const all = projects.flatMap(p => task_list({ project: p }));
const matched = all.filter(t => JSON.parse(t.tags ?? '[]').includes('okr:OKR-H1-3'));

…which is fine until task counts grow. With the filter, the same query is one call and runs in SQL.

The infrastructure was already 90% there — tasks.tags is a JSON-serialized TEXT column, optStringArray helper is used by task_update, and validateTags exists. This PR just wires the read path.

How

AND-mode: list({ tags: ["a", "b"] }) returns tasks that have BOTH a and b. One clause per tag using EXISTS (SELECT 1 FROM json_each(t.tags) WHERE value = ?). SQLite's json_each is in core json1 (compiled into better-sqlite3 by default), so no schema migration or new dependency.

Empty array or omitted = no tag filter (consistent with existing filter conventions).

Changes

File Change
src/types.ts TaskListFilter gains tags?: string[]
src/domain/tasks.ts list() adds AND t.tags IS NOT NULL AND EXISTS (SELECT 1 FROM json_each(t.tags) WHERE value = ?) per tag
src/transport/mcp.ts task_list input schema declares tags: array<string>
src/transport/mcp-handlers.ts handleList passes tags through via optStringArray(args, 'tags')
src/transport/rest.ts GET /api/tasks parses ?tags=a,b (comma-split) and repeated ?tags=a&tags=b

Tests

9 new tests, all passing:

  • tests/tasks.test.ts (5): single tag, AND across multiple tags, empty/no-match, cross-project rollup, tag + project combo.
  • tests/mcp.test.ts (4): single tag via MCP handler, AND-mode via MCP, cross-project rollup via MCP, tag + project combo via MCP.
$ npm run check
> tsc --noEmit                    ✓
> eslint src/ tests/               ✓
> prettier --check .               ✓
> vitest run                       ✓ 440/440 passed (was 431, +9 new)

Open design choices (happy to adjust)

  1. AND-mode default. Followed the principle of least surprise — most use cases want "tasks with all these tags" (e.g., "tasks in this OKR AND in this iteration"). A future tags_any?: string[] for OR-mode would be straightforward.
  2. REST query string parsing. Supports both comma-separated (?tags=okr:H1,iter:H1) and repeated (?tags=okr:H1&tags=iter:H1) for ergonomic flexibility.
  3. No CHANGELOG bump. Left for the maintainer's release flow.

Follow-ups (not in this PR)

  • tags_any?: string[] for OR-mode rollups.
  • tags_exclude?: string[] for "everything not tagged X".
  • Potentially expose tag aggregation (e.g., task_tags_summary returning per-tag counts) for dashboard-side rollup widgets.

🤖 Generated with Claude Code

Enables cross-project tag rollups (e.g., OKR or iteration progress) without
client-side filtering. Currently only the write side (task_create / task_update)
accepted tags — task_list dropped them on the floor.

Implements AND-mode: list({ tags: ["okr:H1-3", "iter:H1"] }) returns tasks
that have BOTH listed tags. Empty array or omitted = no tag filter.

- types.ts: TaskListFilter gains optional tags?: string[]
- domain/tasks.ts: list() adds one EXISTS (json_each(t.tags)) clause per tag
- transport/mcp.ts: task_list inputSchema declares tags array
- transport/mcp-handlers.ts: handleList passes tags through
- transport/rest.ts: GET /api/tasks parses ?tags=a,b or repeated ?tags=
- tests/tasks.test.ts: 5 new domain-level tests (single, AND, empty, cross-project, combined)
- tests/mcp.test.ts: 4 new MCP-handler tests (parity with REST/domain)

All 440 tests pass; npm run check (typecheck + lint + format:check + test) green.
memphyssk pushed a commit to memphyssk/agent-tasks that referenced this pull request May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant