diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 index 5ee7abd8..9ee4389c --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,14 @@ -pnpm exec lint-staged +#!/usr/bin/env sh +cd "$(dirname "$0")/.." || exit 1 + +# Git/IDE hooks often get a minimal PATH — Node lives outside it. +export PATH="$HOME/.local/share/mise/shims:$HOME/.volta/bin:/opt/homebrew/bin:/usr/local/bin:$HOME/.linuxbrew/bin:$PATH" + +if [ -z "${NODE_SKIP_NVM:-}" ] && [ -f "$HOME/.nvm/nvm.sh" ]; then + NVM_DIR="${NVM_DIR:-$HOME/.nvm}" + export NVM_DIR + # shellcheck source=/dev/null + . "$HOME/.nvm/nvm.sh" +fi + +exec ./node_modules/.bin/lint-staged diff --git a/agent-teams-controller/src/internal/taskStore.js b/agent-teams-controller/src/internal/taskStore.js index f5be7d0e..f90dd817 100644 --- a/agent-teams-controller/src/internal/taskStore.js +++ b/agent-teams-controller/src/internal/taskStore.js @@ -117,6 +117,17 @@ function resolveTaskRef(paths, taskRef, options = {}) { } const includeDeleted = options.includeDeleted === true; + + // Fast path: if taskRef looks like a canonical UUID, try direct file read first + if (looksLikeCanonicalTaskId(normalizedRef)) { + const taskPath = getTaskPath(paths, normalizedRef); + const rawTask = readJson(taskPath, null); + if (rawTask && (includeDeleted || rawTask.status !== 'deleted')) { + return normalizedRef; + } + } + + // Fallback: scan all tasks for displayId match or non-UUID refs const tasks = listRawTasks(paths); const exact = tasks.find((task) => task.id === normalizedRef); if (exact && (includeDeleted || exact.status !== 'deleted')) { diff --git a/agent-teams-controller/src/internal/tasks.js b/agent-teams-controller/src/internal/tasks.js index fb27ca12..5205c941 100644 --- a/agent-teams-controller/src/internal/tasks.js +++ b/agent-teams-controller/src/internal/tasks.js @@ -165,6 +165,30 @@ function getTask(context, taskId) { return taskStore.readTask(context.paths, taskId, { includeDeleted: true }); } +function getTaskComment(context, taskId, commentId) { + const normalizedCommentId = String(commentId || '').trim(); + if (!normalizedCommentId) { + throw new Error('Missing commentId'); + } + const task = taskStore.readTask(context.paths, taskId, { includeDeleted: true }); + const comments = Array.isArray(task.comments) ? task.comments : []; + const comment = comments.find((c) => c && c.id === normalizedCommentId); + if (!comment) { + throw new Error(`Comment ${normalizedCommentId} not found on task #${task.displayId || task.id}`); + } + return { + comment, + task: { + id: task.id, + displayId: task.displayId, + subject: task.subject, + status: task.status, + owner: task.owner, + commentCount: comments.length, + }, + }; +} + function listTasks(context) { return taskStore.listTasks(context.paths); } @@ -609,6 +633,7 @@ module.exports = { completeTask, createTask, getTask, + getTaskComment, linkTask, listDeletedTasks, listTasks, diff --git a/mcp-server/src/agent-teams-controller.d.ts b/mcp-server/src/agent-teams-controller.d.ts index fcf7b3e1..2e66083b 100644 --- a/mcp-server/src/agent-teams-controller.d.ts +++ b/mcp-server/src/agent-teams-controller.d.ts @@ -7,6 +7,7 @@ declare module 'agent-teams-controller' { export interface ControllerTaskApi { createTask(flags: Record): unknown; getTask(taskId: string): unknown; + getTaskComment(taskId: string, commentId: string): { comment: Record; task: { id: string; displayId: string; subject: string; status: string; owner: string | null; commentCount: number } }; listTasks(): unknown[]; listDeletedTasks(): unknown[]; resolveTaskId(taskRef: string): string; diff --git a/mcp-server/src/tools/taskTools.ts b/mcp-server/src/tools/taskTools.ts index bbac5d51..f85358ed 100644 --- a/mcp-server/src/tools/taskTools.ts +++ b/mcp-server/src/tools/taskTools.ts @@ -226,6 +226,23 @@ export function registerTaskTools(server: Pick) { await Promise.resolve(jsonTextContent(getController(teamName, claudeDir).tasks.getTask(taskId))), }); + server.addTool({ + name: 'task_get_comment', + description: + 'Get a single task comment by id. Returns the comment object and minimal task context (id, displayId, subject, status, owner).', + parameters: z.object({ + ...toolContextSchema, + taskId: z.string().min(1), + commentId: z.string().min(1), + }), + execute: async ({ teamName, claudeDir, taskId, commentId }) => + await Promise.resolve( + jsonTextContent( + getController(teamName, claudeDir).tasks.getTaskComment(taskId, commentId) + ) + ), + }); + server.addTool({ name: 'task_list', description: 'List tasks for a team', diff --git a/mcp-server/test/tools.test.ts b/mcp-server/test/tools.test.ts index b16b5892..ac3f41e5 100644 --- a/mcp-server/test/tools.test.ts +++ b/mcp-server/test/tools.test.ts @@ -58,6 +58,7 @@ describe('agent-teams-mcp tools', () => { 'task_create', 'task_create_from_message', 'task_get', + 'task_get_comment', 'task_link', 'task_list', 'task_set_clarification',