feat: add search/find command for tasks#1662
feat: add search/find command for tasks#1662withsivram wants to merge 1 commit intoeyaltoledano:mainfrom
Conversation
Adds `task-master search <query>` (aliased as `find`) that searches tasks across title, description, details, and testStrategy fields using case-insensitive substring matching. CLI changes: - New `SearchCommand` at apps/cli/src/commands/search.command.ts - Registered in CommandRegistry under the 'task' category - Exported from @tm/cli index - Supports --status, --tag, --include-subtasks, --format/--json/--compact, --silent, and --project flags - When --include-subtasks is given, also searches subtask title/description/ details/testStrategy and lists matching subtasks under their parent MCP changes: - New `search_tasks` tool at apps/mcp/src/tools/tasks/search-tasks.tool.ts - Registered in mcp-server/src/tools/tool-registry.js (toolRegistry + standardTools) - Parameters: projectRoot, query, status, includeSubtasks, tag - Returns matches with id, title, status, priority, description, and matchingSubtasks plus aggregate stats Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR introduces a task search feature accessible via CLI and MCP tool. Users can search tasks by matching queries case-insensitively against title, description, details, and test-strategy fields, with optional filtering by status and tag. Results include matching subtasks when enabled. Changes
Sequence DiagramsequenceDiagram
actor User
participant CLI/MCP as CLI or MCP Server
participant TmCore as TmCore
participant TaskData as Task Data
User->>CLI/MCP: search command with query + filters
activate CLI/MCP
CLI/MCP->>CLI/MCP: validate status/format options
CLI/MCP->>TmCore: tasks.list(tag, includeSubtasks: true)
activate TmCore
TmCore->>TaskData: fetch all tasks & subtasks
TmCore-->>CLI/MCP: return task list
deactivate TmCore
CLI/MCP->>CLI/MCP: match query against title/description/details/testStrategy
CLI/MCP->>CLI/MCP: filter by status (if provided)
CLI/MCP->>CLI/MCP: filter subtasks & include parent if match found
CLI/MCP->>CLI/MCP: format results (text/json/compact)
CLI/MCP-->>User: return matching tasks with metadata
deactivate CLI/MCP
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds a new task search capability across both the CLI and MCP surfaces, enabling local substring-based search over task fields (and optionally subtasks) to address #1453.
Changes:
- Introduces
task-master search <query>withfindalias, plus filtering/output flags. - Adds a new MCP tool
search_tasksand registers it with the MCP server tool registry. - Wires new command/tool exports and registrations across CLI/MCP registries.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| mcp-server/src/tools/tool-registry.js | Registers search_tasks in the MCP server tool registry and standard tools list. |
| apps/mcp/src/tools/tasks/search-tasks.tool.ts | Implements search_tasks MCP tool with query + optional status/subtask/tag filtering. |
| apps/mcp/src/tools/tasks/index.ts | Exports registerSearchTasksTool from the tasks tool barrel. |
| apps/cli/src/index.ts | Exports the new SearchCommand from the CLI package entry. |
| apps/cli/src/commands/search.command.ts | Implements the new search/find CLI command, including formatting and subtask display. |
| apps/cli/src/command-registry.ts | Registers the search command under the task category. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Build a flat list of tasks (with subtasks collapsed under the parent) for the table | ||
| const tasksForTable = result.matches.map(({ task }) => task); | ||
|
|
||
| console.log( | ||
| ui.createTaskTable(tasksForTable, { | ||
| showSubtasks: options.includeSubtasks, | ||
| showDependencies: true | ||
| }) | ||
| ); |
There was a problem hiding this comment.
In text output, passing showSubtasks: options.includeSubtasks to ui.createTaskTable(...) will render all subtasks for a matching parent task, not just the subtasks that matched the search query. This conflicts with the PR description (“lists matching subtasks under their parent”) and can produce noisy output. Consider either filtering task.subtasks down to matchingSubtasks before building tasksForTable, or keep the table unexpanded and rely solely on the dedicated “Matching subtasks” section.
| // Fetch all tasks (with subtasks so we can search them) | ||
| const listResult = await this.tmCore.tasks.list({ | ||
| tag: options.tag, | ||
| includeSubtasks: true |
There was a problem hiding this comment.
tasks.list({ includeSubtasks: true }) is called unconditionally, even when --include-subtasks is not set. Since tm-core supports excluding subtasks (includeSubtasks: false), this will do extra I/O and memory work for the common case where only top-level fields are searched. Consider passing includeSubtasks: options.includeSubtasks === true (or false when not needed) so subtasks are only loaded when they will actually be searched/displayed.
| // Fetch all tasks (with subtasks so we can search them) | |
| const listResult = await this.tmCore.tasks.list({ | |
| tag: options.tag, | |
| includeSubtasks: true | |
| // Fetch all tasks; include subtasks only when they will be searched | |
| const listResult = await this.tmCore.tasks.list({ | |
| tag: options.tag, | |
| includeSubtasks: options.includeSubtasks === true |
| private async searchTasks( | ||
| query: string, | ||
| options: SearchCommandOptions | ||
| ): Promise<SearchTasksResult> { | ||
| if (!this.tmCore) { | ||
| throw new Error('TmCore not initialized'); | ||
| } | ||
|
|
||
| const queryLower = query.toLowerCase(); | ||
|
|
||
| // Parse optional status filter | ||
| const statusFilter = | ||
| options.status | ||
| ? options.status.split(',').map((s) => s.trim() as TaskStatus) | ||
| : undefined; | ||
|
|
||
| // Fetch all tasks (with subtasks so we can search them) | ||
| const listResult = await this.tmCore.tasks.list({ | ||
| tag: options.tag, | ||
| includeSubtasks: true | ||
| }); | ||
|
|
||
| const matches: TaskMatch[] = []; | ||
|
|
||
| for (const task of listResult.tasks) { | ||
| // Apply status filter if specified | ||
| if (statusFilter && !statusFilter.includes(task.status)) { | ||
| continue; | ||
| } | ||
|
|
||
| const taskMatches = taskMatchesQuery(task, queryLower); | ||
|
|
||
| // Check subtasks | ||
| const matchingSubtasks: Subtask[] = options.includeSubtasks | ||
| ? (task.subtasks || []).filter((st) => | ||
| subtaskMatchesQuery(st, queryLower) | ||
| ) | ||
| : []; | ||
|
|
||
| // Include the task if the task itself matches, or if any subtask matched | ||
| // (when --include-subtasks is set) | ||
| if (taskMatches || matchingSubtasks.length > 0) { | ||
| matches.push({ task, matchingSubtasks }); | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| query, | ||
| matches, | ||
| totalTasks: listResult.total, | ||
| tag: listResult.tag, | ||
| storageType: listResult.storageType as Exclude<StorageType, 'auto'> | ||
| }; | ||
| } |
There was a problem hiding this comment.
There are existing Vitest specs for other CLI commands (e.g. list.command.spec.ts, loop.command.spec.ts), but this new command doesn’t introduce any tests for query matching, status filtering, format flag precedence (--format vs --json/--compact), or --include-subtasks behavior. Adding a search.command.spec.ts would help prevent regressions in the matching logic and output selection.
| // Fetch all tasks (with subtasks for subtask search) | ||
| const listResult = await tmCore.tasks.list({ | ||
| tag, | ||
| includeSubtasks: true |
There was a problem hiding this comment.
tmCore.tasks.list({ includeSubtasks: true }) is executed even when includeSubtasks is not requested. This forces loading subtasks for every search, which can be avoidable overhead on large task sets. Consider setting includeSubtasks: includeSubtasks === true (or false when not needed) so the storage layer can exclude subtasks when you’re only searching top-level task fields.
| // Fetch all tasks (with subtasks for subtask search) | |
| const listResult = await tmCore.tasks.list({ | |
| tag, | |
| includeSubtasks: true | |
| // Fetch all tasks (optionally with subtasks for subtask search) | |
| const listResult = await tmCore.tasks.list({ | |
| tag, | |
| includeSubtasks: includeSubtasks === true |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
apps/mcp/src/tools/tasks/search-tasks.tool.ts (2)
119-123: Minor: Subtasks are always fetched even when not searched.The
includeSubtasks: trueis always passed totmCore.tasks.list()regardless of whetherargs.includeSubtasksis set. This means subtasks are loaded even when they won't be searched. While this keeps the code simple and the overhead is minimal for most projects, you could conditionally set this if performance becomes a concern with large task sets.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/mcp/src/tools/tasks/search-tasks.tool.ts` around lines 119 - 123, The call to tmCore.tasks.list currently always passes includeSubtasks: true, causing subtasks to be fetched even when not requested; update the code around the tmCore.tasks.list invocation to conditionally set includeSubtasks based on the incoming args (e.g., args.includeSubtasks) — either build the options object and only include includeSubtasks when args.includeSubtasks is truthy or set includeSubtasks: !!args.includeSubtasks — so subtasks are fetched only when the user asked for them.
49-70: Consider extracting shared search helpers to@tm/core.The
matchesQuery,taskMatchesQuery, andsubtaskMatchesQueryfunctions are duplicated verbatim between this file andapps/cli/src/commands/search.command.ts. While both CLI and MCP are presentation layers, these pure matching functions could be extracted to@tm/coreto avoid duplication and ensure consistent behavior.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/mcp/src/tools/tasks/search-tasks.tool.ts` around lines 49 - 70, The matching helpers (matchesQuery, taskMatchesQuery, subtaskMatchesQuery) are duplicated in this file and apps/cli/src/commands/search.command.ts; extract these pure functions into a shared module in `@tm/core` (e.g., export matchesQuery, taskMatchesQuery, subtaskMatchesQuery from a new search utilities file) then replace the local definitions with imports from `@tm/core` so both CLI and MCP use the same implementations and avoid duplication.apps/cli/src/commands/search.command.ts (1)
371-379: Consider clarifying subtask display behavior in text format.The table via
createTaskTabledisplays all subtasks of matching tasks (whenshowSubtasks: true), not just the subtasks that matched the query. The "Matching subtasks" section below (lines 382-398) lists only the matching ones. This dual display could be intentional for context, but users might expect the table to show only matching subtasks.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/commands/search.command.ts` around lines 371 - 379, The table currently builds tasksForTable from result.matches.map(({ task }) => task) and then calls ui.createTaskTable(..., { showSubtasks: options.includeSubtasks }), which displays all subtasks when showSubtasks is true; change tasksForTable construction so that for each match you include the parent task but, if options.includeSubtasks is true, replace its subtasks array with only the subtasks that actually matched (use the matchedSubtasks or equivalent field on each match) so the table mirrors the "Matching subtasks" list; keep the original full-subtask behavior when includeSubtasks is false or when no matchedSubtasks data exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/cli/src/commands/search.command.ts`:
- Around line 85-95: The docstring for subtaskMatchesQuery is misleading because
the function checks title, description, details, and testStrategy via
matchesQuery; update the comment above function subtaskMatchesQuery to list all
four fields (title, description, details, testStrategy) that are searched (or
reword to "matches query against subtask fields: title, description, details,
testStrategy") so the documentation matches the implementation.
---
Nitpick comments:
In `@apps/cli/src/commands/search.command.ts`:
- Around line 371-379: The table currently builds tasksForTable from
result.matches.map(({ task }) => task) and then calls ui.createTaskTable(..., {
showSubtasks: options.includeSubtasks }), which displays all subtasks when
showSubtasks is true; change tasksForTable construction so that for each match
you include the parent task but, if options.includeSubtasks is true, replace its
subtasks array with only the subtasks that actually matched (use the
matchedSubtasks or equivalent field on each match) so the table mirrors the
"Matching subtasks" list; keep the original full-subtask behavior when
includeSubtasks is false or when no matchedSubtasks data exists.
In `@apps/mcp/src/tools/tasks/search-tasks.tool.ts`:
- Around line 119-123: The call to tmCore.tasks.list currently always passes
includeSubtasks: true, causing subtasks to be fetched even when not requested;
update the code around the tmCore.tasks.list invocation to conditionally set
includeSubtasks based on the incoming args (e.g., args.includeSubtasks) — either
build the options object and only include includeSubtasks when
args.includeSubtasks is truthy or set includeSubtasks: !!args.includeSubtasks —
so subtasks are fetched only when the user asked for them.
- Around line 49-70: The matching helpers (matchesQuery, taskMatchesQuery,
subtaskMatchesQuery) are duplicated in this file and
apps/cli/src/commands/search.command.ts; extract these pure functions into a
shared module in `@tm/core` (e.g., export matchesQuery, taskMatchesQuery,
subtaskMatchesQuery from a new search utilities file) then replace the local
definitions with imports from `@tm/core` so both CLI and MCP use the same
implementations and avoid duplication.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: bffd3fde-4467-436d-aafd-96ee061a229e
📒 Files selected for processing (6)
apps/cli/src/command-registry.tsapps/cli/src/commands/search.command.tsapps/cli/src/index.tsapps/mcp/src/tools/tasks/index.tsapps/mcp/src/tools/tasks/search-tasks.tool.tsmcp-server/src/tools/tool-registry.js
| /** | ||
| * Return true if a subtask matches the query on title or description. | ||
| */ | ||
| function subtaskMatchesQuery(subtask: Subtask, queryLower: string): boolean { | ||
| return ( | ||
| matchesQuery(subtask.title, queryLower) || | ||
| matchesQuery(subtask.description, queryLower) || | ||
| matchesQuery(subtask.details, queryLower) || | ||
| matchesQuery(subtask.testStrategy, queryLower) | ||
| ); | ||
| } |
There was a problem hiding this comment.
Fix misleading docstring for subtaskMatchesQuery.
The docstring states the function matches "title or description" but the implementation actually checks four fields: title, description, details, and testStrategy.
📝 Proposed fix
/**
- * Return true if a subtask matches the query on title or description.
+ * Return true if a subtask matches the query on any of the searched fields.
+ * Searched fields: title, description, details, testStrategy
*/
function subtaskMatchesQuery(subtask: Subtask, queryLower: string): boolean {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/cli/src/commands/search.command.ts` around lines 85 - 95, The docstring
for subtaskMatchesQuery is misleading because the function checks title,
description, details, and testStrategy via matchesQuery; update the comment
above function subtaskMatchesQuery to list all four fields (title, description,
details, testStrategy) that are searched (or reword to "matches query against
subtask fields: title, description, details, testStrategy") so the documentation
matches the implementation.
Summary
Adds
task-master search <query>(aliased asfind) that searches tasks across title, description, details, and testStrategy fields using case-insensitive substring matching.CLI
SearchCommandatapps/cli/src/commands/search.command.ts--status,--tag,--include-subtasks,--format/--json/--compact,--silent, and--projectflags--include-subtasksis given, also searches subtask fields and lists matching subtasks under their parentMCP
search_taskstool atapps/mcp/src/tools/tasks/search-tasks.tool.tsmcp-server/src/tools/tool-registry.jsprojectRoot,query,status,includeSubtasks,tagTest plan
task-master search "auth"returns tasks with "auth" in title/description/detailstask-master search "auth" --status donefilters by statustask-master search "auth" --include-subtaskssearches and displays subtask matchestask-master find "auth"works as aliassearch_taskstool returns correct results via Claude/CursorCloses #1453
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features