feat: add user-defined metadata field to tasks (#1555) (#1611)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Cedric Hurst <cedric@spantree.net>
Closes #1555
This commit is contained in:
Ralph Khreish
2026-01-26 17:41:33 +01:00
committed by GitHub
parent 364a160775
commit c798639d1a
19 changed files with 2541 additions and 59 deletions

View File

@@ -17,8 +17,9 @@ import { createLogWrapper } from '../../tools/utils.js';
* @param {Object} args - Command arguments containing id, prompt, useResearch, tasksJsonPath, and projectRoot.
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
* @param {string} args.id - Subtask ID in format "parent.sub".
* @param {string} args.prompt - Information to append to the subtask.
* @param {string} [args.prompt] - Information to append to the subtask. Required unless only updating metadata.
* @param {boolean} [args.research] - Whether to use research role.
* @param {Object} [args.metadata] - Parsed metadata object to merge into subtask metadata.
* @param {string} [args.projectRoot] - Project root path.
* @param {string} [args.tag] - Tag for the task (optional)
* @param {Object} log - Logger object.
@@ -27,8 +28,9 @@ import { createLogWrapper } from '../../tools/utils.js';
*/
export async function updateSubtaskByIdDirect(args, log, context = {}) {
const { session } = context;
// Destructure expected args, including projectRoot
const { tasksJsonPath, id, prompt, research, projectRoot, tag } = args;
// Destructure expected args, including projectRoot and metadata
const { tasksJsonPath, id, prompt, research, metadata, projectRoot, tag } =
args;
const logWrapper = createLogWrapper(log);
@@ -60,9 +62,10 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) {
};
}
if (!prompt) {
// At least prompt or metadata is required (validated in MCP tool layer)
if (!prompt && !metadata) {
const errorMessage =
'No prompt specified. Please provide the information to append.';
'No prompt or metadata specified. Please provide information to append or metadata to update.';
logWrapper.error(errorMessage);
return {
success: false,
@@ -77,7 +80,7 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) {
const useResearch = research === true;
log.info(
`Updating subtask with ID ${subtaskIdStr} with prompt "${prompt}" and research: ${useResearch}`
`Updating subtask with ID ${subtaskIdStr} with prompt "${prompt || '(metadata-only)'}" and research: ${useResearch}`
);
const wasSilent = isSilentMode();
@@ -98,7 +101,8 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) {
projectRoot,
tag,
commandName: 'update-subtask',
outputType: 'mcp'
outputType: 'mcp',
metadata
},
'json'
);

View File

@@ -18,9 +18,10 @@ import { findTasksPath } from '../utils/path-utils.js';
* @param {Object} args - Command arguments containing id, prompt, useResearch, tasksJsonPath, and projectRoot.
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
* @param {string} args.id - Task ID (or subtask ID like "1.2").
* @param {string} args.prompt - New information/context prompt.
* @param {string} [args.prompt] - New information/context prompt. Required unless only updating metadata.
* @param {boolean} [args.research] - Whether to use research role.
* @param {boolean} [args.append] - Whether to append timestamped information instead of full update.
* @param {Object} [args.metadata] - Parsed metadata object to merge into task metadata.
* @param {string} [args.projectRoot] - Project root path.
* @param {string} [args.tag] - Tag for the task (optional)
* @param {Object} log - Logger object.
@@ -29,9 +30,17 @@ import { findTasksPath } from '../utils/path-utils.js';
*/
export async function updateTaskByIdDirect(args, log, context = {}) {
const { session } = context;
// Destructure expected args, including projectRoot
const { tasksJsonPath, id, prompt, research, append, projectRoot, tag } =
args;
// Destructure expected args, including projectRoot and metadata
const {
tasksJsonPath,
id,
prompt,
research,
append,
metadata,
projectRoot,
tag
} = args;
const logWrapper = createLogWrapper(log);
@@ -51,9 +60,10 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
};
}
if (!prompt) {
// At least prompt or metadata is required (validated in MCP tool layer)
if (!prompt && !metadata) {
const errorMessage =
'No prompt specified. Please provide a prompt with new information for the task update.';
'No prompt or metadata specified. Please provide a prompt with new information or metadata for the task update.';
logWrapper.error(errorMessage);
return {
success: false,
@@ -95,7 +105,7 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
const useResearch = research === true;
logWrapper.info(
`Updating task with ID ${taskId} with prompt "${prompt}" and research: ${useResearch}`
`Updating task with ID ${taskId} with prompt "${prompt || '(metadata-only)'}" and research: ${useResearch}`
);
const wasSilent = isSilentMode();
@@ -116,7 +126,8 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
projectRoot,
tag,
commandName: 'update-task',
outputType: 'mcp'
outputType: 'mcp',
metadata
},
'json',
append || false

View File

@@ -7,7 +7,8 @@ import { TaskIdSchemaForMcp } from '@tm/core';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
withNormalizedProjectRoot,
validateMcpMetadata
} from '@tm/mcp';
import { z } from 'zod';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -27,11 +28,22 @@ export function registerUpdateSubtaskTool(server) {
id: TaskIdSchemaForMcp.describe(
'ID of the subtask to update in format "parentId.subtaskId" (e.g., "5.2"). Parent ID is the ID of the task that contains the subtask.'
),
prompt: z.string().describe('Information to add to the subtask'),
prompt: z
.string()
.optional()
.describe(
'Information to add to the subtask. Required unless only updating metadata.'
),
research: z
.boolean()
.optional()
.describe('Use Perplexity AI for research-backed updates'),
metadata: z
.string()
.optional()
.describe(
'JSON string of metadata to merge into subtask metadata. Example: \'{"ticketId": "JIRA-456", "reviewed": true}\'. Requires TASK_MASTER_ALLOW_METADATA_UPDATES=true in MCP environment.'
),
file: z.string().optional().describe('Absolute path to the tasks file'),
projectRoot: z
.string()
@@ -65,12 +77,29 @@ export function registerUpdateSubtaskTool(server) {
);
}
// Validate metadata if provided
const validationResult = validateMcpMetadata(
args.metadata,
createErrorResponse
);
if (validationResult.error) {
return validationResult.error;
}
const parsedMetadata = validationResult.parsedMetadata;
// Validate that at least prompt or metadata is provided
if (!args.prompt && !parsedMetadata) {
return createErrorResponse(
'Either prompt or metadata must be provided for update-subtask'
);
}
const result = await updateSubtaskByIdDirect(
{
tasksJsonPath: tasksJsonPath,
id: args.id,
prompt: args.prompt,
research: args.research,
metadata: parsedMetadata,
projectRoot: args.projectRoot,
tag: resolvedTag
},

View File

@@ -6,7 +6,8 @@
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
withNormalizedProjectRoot,
validateMcpMetadata
} from '@tm/mcp';
import { z } from 'zod';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -30,7 +31,10 @@ export function registerUpdateTaskTool(server) {
),
prompt: z
.string()
.describe('New information or context to incorporate into the task'),
.optional()
.describe(
'New information or context to incorporate into the task. Required unless only updating metadata.'
),
research: z
.boolean()
.optional()
@@ -41,6 +45,12 @@ export function registerUpdateTaskTool(server) {
.describe(
'Append timestamped information to task details instead of full update'
),
metadata: z
.string()
.optional()
.describe(
'JSON string of metadata to merge into task metadata. Example: \'{"githubIssue": 42, "sprint": "Q1-S3"}\'. Requires TASK_MASTER_ALLOW_METADATA_UPDATES=true in MCP environment.'
),
file: z.string().optional().describe('Absolute path to the tasks file'),
projectRoot: z
.string()
@@ -76,7 +86,23 @@ export function registerUpdateTaskTool(server) {
);
}
// 3. Call Direct Function - Include projectRoot
// Validate metadata if provided
const validationResult = validateMcpMetadata(
args.metadata,
createErrorResponse
);
if (validationResult.error) {
return validationResult.error;
}
const parsedMetadata = validationResult.parsedMetadata;
// Validate that at least prompt or metadata is provided
if (!args.prompt && !parsedMetadata) {
return createErrorResponse(
'Either prompt or metadata must be provided for update-task'
);
}
// Call Direct Function - Include projectRoot and metadata
const result = await updateTaskByIdDirect(
{
tasksJsonPath: tasksJsonPath,
@@ -84,6 +110,7 @@ export function registerUpdateTaskTool(server) {
prompt: args.prompt,
research: args.research,
append: args.append,
metadata: parsedMetadata,
projectRoot: args.projectRoot,
tag: resolvedTag
},