feat: add user-defined metadata field to tasks (#1555)#1610
feat: add user-defined metadata field to tasks (#1555)#1610Crunchyman-ralph wants to merge 14 commits intonextfrom
Conversation
- Add subtask metadata preservation in FileStorage.updateTask - Add subtask metadata preservation in legacy update-task-by-id.js - Add test for subtask metadata preservation during AI updates Fixes Cursor Bugbot review comment about subtask metadata loss
Replace risky position-based fallback with: - Type-coerced ID comparison (handles string vs number) - Title-based fallback (subtask titles are typically unique) This prevents metadata from being assigned to wrong subtask if AI reorders subtasks or returns IDs with type mismatches.
Add optional `metadata` field to tasks and subtasks for storing arbitrary user-defined JSON data (external IDs, workflow data, integration references, etc.). Key features: - AI-Safe: Metadata excluded from AI schemas, preserved through all operations - MCP Support: update_task/update_subtask tools accept metadata parameter - Safety Flag: MCP updates require TASK_MASTER_ALLOW_METADATA_UPDATES=true - Metadata Merge: New metadata merges with existing, preserving unmodified fields Closes #1555 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When both prompt and metadata are provided to update-subtask, the metadata was being ignored. Now metadata is merged after AI update, similar to how update-task handles it. Fixes Cursor Bugbot review comment about ignored metadata.
…servation Use String(st.id) === String(updatedSubtask.id) instead of strict equality to handle type mismatches (AI may return string IDs vs numeric). Also add title-based fallback matching. Addresses CodeRabbit duplicate comment about FileStorage ID matching.
- Add validateMcpMetadata() utility function in tools/utils.js - Replace duplicated validation code in update-task.js and update-subtask.js - Reduces code duplication and ensures consistent validation Addresses CodeRabbit nitpick about duplicated metadata validation.
Clarifies that these tests focus on validation logic while end-to-end behavior is covered by FileStorage and AI operation tests. Addresses CodeRabbit nitpick about test structure.
Changed from replacing metadata to merging it: - Preserves existing metadata keys from original subtask - Adds/overrides with new metadata keys from update - Supports both AI updates (no metadata) and direct updates (with metadata) Addresses CodeRabbit nitpick about metadata replacement.
Critical fixes for update-subtask-by-id.js: 1. Added 'metadata' to context destructuring (was causing ReferenceError) 2. Updated prompt validation to allow metadata-only updates Without these fixes, subtask metadata updates would fail at runtime. Fixes Cursor Bugbot HIGH severity issues.
- Add useResearch and metadata to UpdateBridgeParams interface - Pass both parameters through to tmCore.tasks.updateWithPrompt - Update both update-task-by-id.js and update-subtask-by-id.js - Improve error message in validateMcpMetadata to include parse error Fixes CodeRabbit outside-diff concern about silently lost parameters. Addresses CodeRabbit nitpick about error message detail.
- Move validateMcpMetadata to @tm/mcp shared utils (TypeScript) - Remove duplicate implementation from mcp-server/src/tools/utils.js - Add comprehensive unit tests for validateMcpMetadata function - Fix dependencies type in metadata-preservation test (number → string) - Fix TypeScript union type handling in test assertions
🦋 Changeset detectedLatest commit: 07e3e66 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📝 WalkthroughWalkthroughThis PR adds optional metadata support to tasks and subtasks, enabling storage of arbitrary user-defined JSON data (external IDs, integration info, workflow data). Includes metadata validation, MCP tool integration, storage layer merging logic, and comprehensive test coverage for preservation across AI operations and updates. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
mcp-server/src/tools/update-subtask.js (1)
61-66: Sanitize logged arguments to avoid leaking metadata.Logging full args now includes user-provided metadata (and potentially prompt text). Please log a safe subset (IDs/flags) instead of raw inputs. As per coding guidelines.
🛡️ Proposed fix
- log.info(`Updating subtask with args: ${JSON.stringify(args)}`); + const safeArgs = { + id: args.id, + tag: resolvedTag, + hasPrompt: Boolean(args.prompt?.trim()), + hasMetadata: Boolean(args.metadata) + }; + log.info(`Updating subtask with args: ${JSON.stringify(safeArgs)}`);scripts/modules/task-manager/update-task-by-id.js (1)
501-530: Guard title-based metadata matching to avoid mis-association.If titles are duplicated, the fallback can attach metadata to the wrong subtask. Consider only using title matches when they’re unique (or skip metadata on ambiguity).
🧩 Safer matching example
- const originalSubtask = taskToUpdate.subtasks?.find( - (st) => - String(st.id) === String(subtask.id) || - (subtask.title && st.title === subtask.title) - ); + const idMatch = taskToUpdate.subtasks?.find( + (st) => String(st.id) === String(subtask.id) + ); + const titleMatches = subtask.title + ? taskToUpdate.subtasks?.filter( + (st) => st.title === subtask.title + ) ?? [] + : []; + const originalSubtask = + idMatch ?? (titleMatches.length === 1 ? titleMatches[0] : undefined);
🤖 Fix all issues with AI agents
In `@apps/docs/capabilities/task-structure.mdx`:
- Around line 138-147: The "Manual edit" row in the Metadata Behavior table
currently states that you can add/modify metadata directly in tasks.json; change
this to discourage direct edits by rewording the row to something like "Manual
edit (discouraged) — Avoid editing tasks.json directly; use Task Master commands
or MCP (use the metadata parameter) to update metadata" and add a short
parenthetical or footnote pointing readers to Task Master commands and MCP as
the preferred update mechanisms (referencing the terms tasks.json, Task Master,
MCP, and metadata parameter to locate and update the table entry).
In `@packages/tm-core/src/modules/tasks/entities/task.entity.spec.ts`:
- Around line 213-240: The test fails because TaskEntity.toJSON() serializes the
task field as "Details" (capital D) while the Task interface and tests expect
"details" (lowercase); update the TaskEntity.toJSON method in the TaskEntity
class to emit the property name "details" (lowercase) instead of "Details" so
that the output keys match the Task shape and the spec assertions (e.g.,
json.details) pass. Ensure any mapping logic inside TaskEntity.toJSON or its
serializer uses the exact lowercase key and update any related property mappings
in TaskEntity if present.
🧹 Nitpick comments (2)
packages/tm-core/tests/integration/storage/file-storage-metadata.test.ts (1)
51-471: Consider adding legacy/tagged format coverage for metadata preservation.These tests exercise the default/master format only. Project guidance calls for new features to be tested against both legacy and tagged task data formats, so a small case for a legacy
{tasks: [...]}file and a non‑master tag would help. Based on learnings, add coverage for legacy/tagged formats and tag resolution.packages/tm-core/tests/integration/ai-operations/metadata-preservation.test.ts (1)
62-62: Add tagged/legacy + error-path coverage for metadata preservation.This suite covers untagged success paths only. Please add a tagged task-list scenario (non-default tag) and verify tag resolution/legacy migration behavior, plus at least one negative case (e.g., invalid metadata payload or missing task id) to cover error handling around metadata updates. Based on learnings and coding guidelines.
| ### Metadata Behavior | ||
|
|
||
| | Operation | Metadata Behavior | | ||
| | ---------------- | ------------------------------------------------------------ | | ||
| | `parse-prd` | New tasks are created without metadata | | ||
| | `update-task` | Existing metadata is preserved unless explicitly changed | | ||
| | `expand` | Parent task metadata is preserved; subtasks don't inherit it | | ||
| | `update-subtask` | Subtask metadata is preserved | | ||
| | Manual edit | You can add/modify metadata directly in tasks.json | | ||
| | MCP (with flag) | Use the `metadata` parameter to explicitly update metadata | |
There was a problem hiding this comment.
Manual-edit guidance conflicts with project rules.
The table says metadata can be edited directly in tasks.json, but project guidance discourages manual edits of that file. Consider softening to “discouraged” and point users to Task Master commands/MCP updates instead. Based on learnings, align docs with the “do not manually edit tasks.json” guidance.
📝 Suggested doc tweak
-| Manual edit | You can add/modify metadata directly in tasks.json |
+| Manual edit | Discouraged; prefer Task Master commands or MCP updates |📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ### Metadata Behavior | |
| | Operation | Metadata Behavior | | |
| | ---------------- | ------------------------------------------------------------ | | |
| | `parse-prd` | New tasks are created without metadata | | |
| | `update-task` | Existing metadata is preserved unless explicitly changed | | |
| | `expand` | Parent task metadata is preserved; subtasks don't inherit it | | |
| | `update-subtask` | Subtask metadata is preserved | | |
| | Manual edit | You can add/modify metadata directly in tasks.json | | |
| | MCP (with flag) | Use the `metadata` parameter to explicitly update metadata | | |
| ### Metadata Behavior | |
| | Operation | Metadata Behavior | | |
| | ---------------- | ------------------------------------------------------------ | | |
| | `parse-prd` | New tasks are created without metadata | | |
| | `update-task` | Existing metadata is preserved unless explicitly changed | | |
| | `expand` | Parent task metadata is preserved; subtasks don't inherit it | | |
| | `update-subtask` | Subtask metadata is preserved | | |
| | Manual edit | Discouraged; prefer Task Master commands or MCP updates | | |
| | MCP (with flag) | Use the `metadata` parameter to explicitly update metadata | |
🤖 Prompt for AI Agents
In `@apps/docs/capabilities/task-structure.mdx` around lines 138 - 147, The
"Manual edit" row in the Metadata Behavior table currently states that you can
add/modify metadata directly in tasks.json; change this to discourage direct
edits by rewording the row to something like "Manual edit (discouraged) — Avoid
editing tasks.json directly; use Task Master commands or MCP (use the metadata
parameter) to update metadata" and add a short parenthetical or footnote
pointing readers to Task Master commands and MCP as the preferred update
mechanisms (referencing the terms tasks.json, Task Master, MCP, and metadata
parameter to locate and update the table entry).
| it('should preserve all task fields alongside metadata', () => { | ||
| const metadata = { custom: 'data' }; | ||
| const task = createMinimalTask({ | ||
| id: '42', | ||
| title: 'Important Task', | ||
| description: 'Do the thing', | ||
| status: 'in-progress', | ||
| priority: 'high', | ||
| dependencies: ['1', '2'], | ||
| details: 'Detailed info', | ||
| testStrategy: 'Unit tests', | ||
| tags: ['urgent'], | ||
| metadata | ||
| }); | ||
|
|
||
| const entity = new TaskEntity(task); | ||
| const json = entity.toJSON(); | ||
|
|
||
| expect(json.id).toBe('42'); | ||
| expect(json.title).toBe('Important Task'); | ||
| expect(json.description).toBe('Do the thing'); | ||
| expect(json.status).toBe('in-progress'); | ||
| expect(json.priority).toBe('high'); | ||
| expect(json.dependencies).toEqual(['1', '2']); | ||
| expect(json.details).toBe('Detailed info'); | ||
| expect(json.testStrategy).toBe('Unit tests'); | ||
| expect(json.tags).toEqual(['urgent']); | ||
| expect(json.metadata).toEqual(metadata); |
There was a problem hiding this comment.
TaskEntity.toJSON() likely emits Details, so this assertion will fail.
The test expects json.details, but packages/tm-core/src/modules/tasks/entities/task.entity.ts currently returns Details (capital D). This will break these assertions and also diverges from the Task interface. Consider aligning the serializer to use details.
🛠️ Suggested fix in packages/tm-core/src/modules/tasks/entities/task.entity.ts
- Details: this.details,
+ details: this.details,🤖 Prompt for AI Agents
In `@packages/tm-core/src/modules/tasks/entities/task.entity.spec.ts` around lines
213 - 240, The test fails because TaskEntity.toJSON() serializes the task field
as "Details" (capital D) while the Task interface and tests expect "details"
(lowercase); update the TaskEntity.toJSON method in the TaskEntity class to emit
the property name "details" (lowercase) instead of "Details" so that the output
keys match the Task shape and the spec assertions (e.g., json.details) pass.
Ensure any mapping logic inside TaskEntity.toJSON or its serializer uses the
exact lowercase key and update any related property mappings in TaskEntity if
present.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| mode | ||
| mode, | ||
| useResearch, | ||
| ...(metadata && { metadata }) |
There was a problem hiding this comment.
Metadata updates silently fail for API storage users
High Severity
The bridge passes metadata to tmCore.tasks.updateWithPrompt(), but the updateTaskWithPrompt interface and API storage implementation don't include metadata in their options type. In api-storage.ts, only { prompt, mode } is sent to the API endpoint, silently ignoring any metadata. API storage users attempting metadata updates via MCP tools will see success messages while their metadata changes are never persisted.
What type of PR is this?
Description
Add optional
metadatafield to tasks and subtasks for storing arbitrary user-defined JSON data. This allows external integrations to link tasks to GitHub issues, Jira tickets, track sprints, story points, and other workflow data without schema changes.Key features:
update_taskandupdate_subtasktools accept ametadataJSON parameterTASK_MASTER_ALLOW_METADATA_UPDATES=trueenv varRelated Issues
Fixes #1555
How to Test This
Expected result:
Contributor Checklist
npm run changesetnpm test(metadata tests pass; unrelated task-id validation tests have pre-existing failures)npm run format-check(ornpm run formatto fix)Changelog Entry
Add optional
metadatafield to tasks for storing user-defined custom data (external IDs, workflow data, integration references)For Maintainers
Note
Introduces a flexible
metadatafield on tasks and subtasks, preserved across all AI operations and storage paths, with MCP tooling to safely update it.Task.metadata; ensure serialization preserves it; FileStorage merges/preserves task and subtask metadata during updates (including AI-generated subtasks)update_task/update_subtaskacceptmetadata(JSON string), support metadata-only updates, and requireTASK_MASTER_ALLOW_METADATA_UPDATES=true; newvalidateMcpMetadatautilitymetadatathrough update flows; remote updates forwardmetadata; improved logging and validationmetadatato prevent overwritesWritten by Cursor Bugbot for commit dada391. This will update automatically on new commits. Configure here.
Summary by CodeRabbit
New Features
Documentation
Tests
✏️ Tip: You can customize this high-level summary in your review settings.